mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
276 Commits
version/1.
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
2de9b87c6f | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
79f4e69a05 | ||
|
|
f13148af3d | ||
|
|
d2eb2d2941 | ||
|
|
942c0ee113 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
dbf8af1146 | ||
|
|
d7ccf1454e | ||
|
|
652d447f8b | ||
|
|
2f58379feb | ||
|
|
c14d3f4274 | ||
|
|
d4673a2f07 | ||
|
|
3a0c94577f | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
c407b73c22 | ||
|
|
361add3385 | ||
|
|
9d6ab69d37 | ||
|
|
0f6da35c0b | ||
|
|
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 | ||
|
|
bd25097e44 | ||
|
|
3f140e818f | ||
|
|
dbba22131c | ||
|
|
8908828af0 | ||
|
|
20c9e1d406 | ||
|
|
741576526d | ||
|
|
b928ec112e | ||
|
|
559026b65c | ||
|
|
eb6fef63b3 | ||
|
|
1b767f8d26 | ||
|
|
3b061cda89 | ||
|
|
e0d7bb50f3 | ||
|
|
20e8e9167f | ||
|
|
268ede869d | ||
|
|
8b8ddaf31b | ||
|
|
f16fbeade7 | ||
|
|
d27677030d | ||
|
|
578e912faf | ||
|
|
bd8c6caa39 | ||
|
|
7635230c88 | ||
|
|
eb0d746082 | ||
|
|
61b3853390 | ||
|
|
76485cdd56 | ||
|
|
3d5401a8db | ||
|
|
6faecc2874 | ||
|
|
4650ca40c1 | ||
|
|
14ee26557a | ||
|
|
8e873013a9 | ||
|
|
b0caf46406 | ||
|
|
a0926c4064 | ||
|
|
b795b1c5ce | ||
|
|
61ba5f5c39 | ||
|
|
f177287fb6 | ||
|
|
ef929e78f3 | ||
|
|
274a55e257 | ||
|
|
9e24223085 | ||
|
|
3cd3d8bdcc | ||
|
|
eed3f2ff4c | ||
|
|
b09b4b1d17 | ||
|
|
cd9c667e8a | ||
|
|
2675e79dbd | ||
|
|
ef94f55c82 | ||
|
|
926919524b | ||
|
|
087f691544 | ||
|
|
3a81ebf0e2 | ||
|
|
3ffaaab8f3 | ||
|
|
5a98da2031 | ||
|
|
2e4014863f | ||
|
|
d3fcb42a8f | ||
|
|
51be153527 | ||
|
|
5728b7c8a8 | ||
|
|
71b6907c31 | ||
|
|
eead96ca26 | ||
|
|
f99e8770f0 | ||
|
|
f6ab59ceda | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
f1004e2e36 | ||
|
|
4e88cbf318 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
b5e53ec25c | ||
|
|
26eaec4c72 | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
d0419edb92 | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 | ||
|
|
f972733426 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: helloanoop
|
||||
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/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -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",
|
||||
},
|
||||
}
|
||||
]);
|
||||
2499
package-lock.json
generated
2499
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
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,6 +38,7 @@
|
||||
},
|
||||
"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:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
@@ -38,6 +46,8 @@
|
||||
"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 +57,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "1.39.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,33 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'State'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientCredentials;
|
||||
@@ -1,21 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,54 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.grant-type-mode-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grant-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const GrantTypeSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('authorization_code');
|
||||
}}
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('client_credentials');
|
||||
}}
|
||||
>
|
||||
Client Credentials
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,29 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,21 +1,33 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
const grantTypeComponentMap = (grantType, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials collection={collection} />;
|
||||
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode collection={collection} />;
|
||||
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials collection={collection} />;
|
||||
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -24,12 +36,12 @@ const grantTypeComponentMap = (grantType, collection) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, collection)}
|
||||
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
|
||||
<GrantTypeComponentMap collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string().required(),
|
||||
domain: Yup.string()
|
||||
.required()
|
||||
.trim()
|
||||
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
|
||||
type: Yup.string().required().oneOf(['cert', 'pfx']),
|
||||
certFilePath: Yup.string().when('type', {
|
||||
is: (type) => type == 'cert',
|
||||
@@ -45,7 +48,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
let relevantValues = {};
|
||||
if (values.type === 'cert') {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
certFilePath: values.certFilePath,
|
||||
keyFilePath: values.keyFilePath,
|
||||
@@ -53,7 +56,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
};
|
||||
} else {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
pfxFilePath: values.pfxFilePath,
|
||||
passphrase: values.passphrase
|
||||
@@ -127,15 +130,20 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox non-passphrase-input"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
|
||||
https://
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="example.org"
|
||||
className="block textbox non-passphrase-input !pl-[60px]"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
|
||||
@@ -119,6 +119,6 @@ This documentation supports Markdown formatting! You can use:
|
||||
- **Bold** and *italic* text
|
||||
- \`code blocks\` and syntax highlighting
|
||||
- Tables and lists
|
||||
- [Links](https://example.com)
|
||||
- [Links](https://usebruno.com)
|
||||
- And more!
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
export default Wrapper;
|
||||
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
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();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
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 (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={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;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire folder. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center mb-4">
|
||||
<AuthMode collection={collection} folder={folder} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.auth-mode-selector {
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateFolderAuthMode({
|
||||
mode: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<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={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
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={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMode;
|
||||
@@ -8,7 +8,9 @@ import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -26,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;
|
||||
|
||||
@@ -37,6 +39,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updatedFolderSettingsSelectedTab({
|
||||
@@ -61,6 +66,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation collection={collection} folder={folder} />;
|
||||
}
|
||||
@@ -93,6 +101,10 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Vars
|
||||
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -11,6 +11,47 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,28 +1,63 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
state,
|
||||
pkce,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -40,7 +75,15 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
state,
|
||||
scope,
|
||||
pkce,
|
||||
[key]: value
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -61,30 +104,35 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
clientSecret,
|
||||
state,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -99,8 +147,33 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<label className="block">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
@@ -108,16 +181,154 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -11,6 +11,47 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -34,6 +69,14 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -42,12 +85,21 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -62,9 +114,179 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,18 +3,20 @@ import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { IconCaretDown, IconKey } from '@tabler/icons';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const GrantTypeSelector = ({ item, collection }) => {
|
||||
const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const [valuesCache, setValuesCache] = useState({
|
||||
...oAuth
|
||||
});
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
@@ -24,13 +26,19 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
let updatedValues = {
|
||||
...valuesCache,
|
||||
...oAuth,
|
||||
grantType
|
||||
};
|
||||
setValuesCache(updatedValues);
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType
|
||||
...updatedValues
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -46,7 +54,18 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
scope: '',
|
||||
credentialsPlacement: 'body',
|
||||
credentialsId: 'credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token',
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -54,7 +73,14 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="flex items-center gap-2.5 my-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Grant Type
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
@@ -89,4 +115,4 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
export default GrantTypeSelector;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from 'react-hot-toast';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getAllVariables } from "utils/collections/index";
|
||||
|
||||
const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
const [refreshingToken, toggleRefreshingToken] = useState(false);
|
||||
|
||||
const interpolatedAccessTokenUrl = useMemo(() => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
return interpolate(accessTokenUrl, variables);
|
||||
}, [collection, item, accessTokenUrl]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleFetchingToken(false);
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
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 occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 mt-4">
|
||||
<button onClick={handleFetchOauth2Credentials} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
{creds?.refresh_token ? <button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button> : null}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Oauth2ActionButtons;
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
ol[role="tree"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
ol[role="group"] span {
|
||||
line-break: anywhere;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { find } from "lodash";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
|
||||
const TokenSection = ({ title, token }) => {
|
||||
if (!token) return null;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [decodedToken, setDecodedToken] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length === 3) {
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
setDecodedToken(payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error decoding token:', err);
|
||||
}
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleCopy = async (text) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2 border dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
{isExpanded ?
|
||||
<IconChevronDown size={18} className="text-gray-500" /> :
|
||||
<IconChevronRight size={18} className="text-gray-500" />
|
||||
}
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="p-3 text-sm">
|
||||
<div className="relative group">
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleCopy(token)}
|
||||
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
|
||||
title="Copy token"
|
||||
>
|
||||
{copied ?
|
||||
<IconCheck size={16} className="text-green-700" /> :
|
||||
<IconCopy size={16} className="text-gray-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all">
|
||||
{token}
|
||||
</div>
|
||||
</div>
|
||||
{decodedToken && (
|
||||
<div className="mt-3">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Decoded Payload</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
||||
{Object.entries(decodedToken).map(([key, value]) => (
|
||||
<div key={key} className="overflow-hidden text-ellipsis">
|
||||
<span className="font-medium text-xs">{key}: </span>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-300">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const formatExpiryTime = (seconds) => {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||
};
|
||||
|
||||
const ExpiryTimer = ({ expiresIn }) => {
|
||||
if (!expiresIn) return null;
|
||||
|
||||
const calculateTimeLeft = () => Math.max(0, Math.floor(expiresIn - Date.now() / 1000));
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeLeft(calculateTimeLeft());
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [expiresIn]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30
|
||||
? "bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400"
|
||||
: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
|
||||
}`}
|
||||
>
|
||||
{timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
|
||||
const interpolatedUrl = useMemo(() => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
return interpolate(url, variables);
|
||||
}, [collection, item, url]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="relative w-auto h-fit mt-2">
|
||||
{Object.keys(creds)?.length ? (
|
||||
creds?.error ? (
|
||||
<pre className="text-red-600 dark:text-red-400">Error fetching token. Check network logs for more details.</pre>
|
||||
) : (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm">
|
||||
<TokenSection title="Access Token" token={creds.access_token} />
|
||||
<TokenSection title="Refresh Token" token={creds.refresh_token} />
|
||||
<TokenSection title="ID Token" token={creds.id_token} />
|
||||
{(creds.token_type || creds.scope) ? <div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{creds.token_type ? <div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Token Type:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
|
||||
</div> : null}
|
||||
{creds?.scope ? <div className="flex items-center space-x-1 min-w-0">
|
||||
<span className="font-medium flex-shrink-0">Scope:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
|
||||
{creds.scope}
|
||||
</span>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div> : null}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">No token found</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Oauth2TokenViewer;
|
||||
@@ -11,6 +11,47 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
const {
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
const handleSave = () => { save(); }
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -36,6 +72,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -44,12 +88,21 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,11 +117,179 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</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" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
export default OAuth2PasswordCredentials;
|
||||
|
||||
@@ -5,17 +5,34 @@ import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const GrantTypeComponentMap = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
|
||||
const grantTypeComponentMap = (grantType, item, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials item={item} collection={collection} />;
|
||||
return <OAuth2PasswordCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode item={item} collection={collection} />;
|
||||
return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials item={item} collection={collection} />;
|
||||
return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -24,12 +41,12 @@ const grantTypeComponentMap = (grantType, item, collection) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ item, collection }) => {
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector item={item} collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
|
||||
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
|
||||
<GrantTypeComponentMap item={item} collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,71 +7,109 @@ 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';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import OAuth2 from './OAuth2/index';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
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', {});
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
// Save function for request level
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case '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();
|
||||
return (
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
{collectionAuth?.mode === 'oauth2' ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-1">
|
||||
<div>Collection level auth is: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-50">
|
||||
Note: You need to use scripting to set the access token in the request headers.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>Auth inherited from the Collection: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1">
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && (
|
||||
item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
<ErrorIndicator /> :
|
||||
<ContentIndicator />
|
||||
)}
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { clearRequestTimeline } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const ClearTimeline = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clearResponse = () =>
|
||||
dispatch(
|
||||
clearRequestTimeline({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={clearResponse} className='text-link hover:underline' title="Clear Timeline">
|
||||
Clear Timeline
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearTimeline;
|
||||
@@ -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" />
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
|
||||
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (data === undefined || !dataBuffer) {
|
||||
if (data === undefined || !dataBuffer || !mode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const formatErrorMessage = (error) => {
|
||||
|
||||
const remoteMethodError = "Error invoking remote method 'send-http-request':";
|
||||
|
||||
if (error.includes(remoteMethodError)) {
|
||||
if (error?.includes(remoteMethodError)) {
|
||||
const parts = error.split(remoteMethodError);
|
||||
return parts[1]?.trim() || error;
|
||||
}
|
||||
@@ -92,7 +92,9 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
// Always show raw
|
||||
const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
|
||||
|
||||
if (mode.includes('html') && typeof data === 'string') {
|
||||
if (!mode || !contentType) return allowedPreviewModes;
|
||||
|
||||
if (mode?.includes('html') && typeof data === 'string') {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
|
||||
@@ -140,7 +142,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="w-full h-full relative"
|
||||
className="w-full h-full relative flex"
|
||||
style={{ maxWidth: width }}
|
||||
queryFilterEnabled={queryFilterEnabled}
|
||||
>
|
||||
|
||||
@@ -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,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunnerTimeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
<pre className="line request font-bold">
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<pre className="line response font-bold">
|
||||
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunnerTimeline;
|
||||
@@ -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;
|
||||
@@ -12,6 +12,10 @@ const StyledWrapper = styled.div`
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,11 +1,109 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.timeline-event {
|
||||
padding: 8px 0 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-event-content {
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.timeline-event-header {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.method-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.oauth-section {
|
||||
.oauth-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-switcher {
|
||||
border-bottom: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color};
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oauth-request-item-content {
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsible-section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.section-header {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
@@ -19,6 +117,35 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
|
||||
.request-label {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 8px;
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import QueryResult from "components/ResponsePane/QueryResult/index";
|
||||
import { useState } from "react";
|
||||
|
||||
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }) => {
|
||||
const [isBodyCollapsed, toggleBody] = useState(true);
|
||||
return (
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
|
||||
</pre>
|
||||
</div>
|
||||
{isBodyCollapsed && (
|
||||
<div className="mt-2">
|
||||
{data || dataBuffer ? (
|
||||
<div className="h-96 overflow-auto">
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
headers={headers}
|
||||
error={error}
|
||||
key={item?.uid}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500">No Body found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BodyBlock;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const HeadersBlock = ({ headers, type }) => {
|
||||
const [areHeadersCollapsed, toggleHeaders] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="collapsible-section mt-2">
|
||||
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
|
||||
{headers && Object.keys(headers).length > 0 &&
|
||||
<div className="ml-1">({Object.keys(headers).length})</div>
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
{areHeadersCollapsed && (
|
||||
<div className="mt-1">
|
||||
{headers && Object.keys(headers).length > 0
|
||||
? <Headers headers={headers} type={type} />
|
||||
: <div className="text-gray-500">No Headers found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const Headers = ({ headers, type }) => {
|
||||
if (Array.isArray(headers)) {
|
||||
return (
|
||||
<div className="mt-1 text-sm">
|
||||
{headers.map((header, index) => (
|
||||
<pre key={index} className="mb-1 whitespace-pre-wrap">
|
||||
{type === 'request' ? '>' : '<'} <span className="opacity-60">{header?.name}:</span>
|
||||
<span className="whitespace-pre-wrap">{String(header?.value)}</span>
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mt-1 text-sm">
|
||||
{Object.entries(headers).map(([key, value], index) => (
|
||||
<pre key={index} className="mb-1 whitespace-pre-wrap">
|
||||
{type === 'request' ? '>' : '<'} <span className="opacity-60">{key}:</span>
|
||||
<span>{String(value)}</span>
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default HeadersBlock;
|
||||
@@ -0,0 +1,19 @@
|
||||
const Method = ({ method }) => {
|
||||
return (
|
||||
<span className={`${methodColors[method?.toUpperCase()] || 'text-white'} font-bold`}>
|
||||
{method?.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const methodColors = {
|
||||
GET: 'text-green-500',
|
||||
POST: 'text-blue-500',
|
||||
PUT: 'text-yellow-500',
|
||||
DELETE: 'text-red-500',
|
||||
PATCH: 'text-purple-500',
|
||||
OPTIONS: 'text-gray-500',
|
||||
HEAD: 'text-gray-500',
|
||||
};
|
||||
|
||||
export default Method;
|
||||
@@ -0,0 +1,26 @@
|
||||
const Status = ({ statusCode, statusText }) => {
|
||||
return (
|
||||
<span
|
||||
className={`${
|
||||
statusColor(statusCode) || 'text-white'
|
||||
} font-bold`}
|
||||
>
|
||||
{statusCode}{' '}
|
||||
{statusText || ''}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const statusColor = (statusCode) => {
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return 'text-green-500';
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
return 'text-yellow-500';
|
||||
} else if (statusCode >= 400 && statusCode < 600) {
|
||||
return 'text-red-500';
|
||||
} else {
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const getRelativeTime = (date) => {
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
||||
const diff = (date - new Date()) / 1000;
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: 'year', seconds: 31536000 },
|
||||
{ unit: 'month', seconds: 2592000 },
|
||||
{ unit: 'week', seconds: 604800 },
|
||||
{ unit: 'day', seconds: 86400 },
|
||||
{ unit: 'hour', seconds: 3600 },
|
||||
{ unit: 'minute', seconds: 60 },
|
||||
{ unit: 'second', seconds: 1 }
|
||||
];
|
||||
|
||||
for (const { unit, seconds } of timeUnits) {
|
||||
if (Math.abs(diff) >= seconds || unit === 'second') {
|
||||
return rtf.format(Math.round(diff / seconds), unit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const RelativeTime = ({ timestamp }) => {
|
||||
const [relativeTime, setRelativeTime] = useState(getRelativeTime(new Date(timestamp)));
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setRelativeTime(getRelativeTime(new Date(timestamp)));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [timestamp]);
|
||||
|
||||
return <pre className="text-xs">{relativeTime}</pre>;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
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((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>
|
||||
)
|
||||
}
|
||||
|
||||
const NetworkLogsEntry = ({ entry }) => {
|
||||
const { type, message } = entry;
|
||||
let className = '';
|
||||
|
||||
switch (type) {
|
||||
case 'request':
|
||||
className = 'text-blue-500';
|
||||
break;
|
||||
case 'response':
|
||||
className = 'text-green-500';
|
||||
break;
|
||||
case 'error':
|
||||
className = 'text-red-500';
|
||||
break;
|
||||
case 'tls':
|
||||
className = 'text-purple-500';
|
||||
break;
|
||||
case 'info':
|
||||
className = 'text-yellow-500';
|
||||
break;
|
||||
default:
|
||||
className = 'text-gray-400';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Network;
|
||||
@@ -0,0 +1,41 @@
|
||||
import Headers from "../Common/Headers/index";
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
|
||||
const safeStringifyJSONIfNotString = (obj) => {
|
||||
if (obj === null || obj === undefined) return '';
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return '[Unserializable Object]';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const Request = ({ collection, request, item, width }) => {
|
||||
let { url, headers, data, dataBuffer, error } = request || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Method and URL */}
|
||||
<div className="mb-1 flex gap-2">
|
||||
<pre className="whitespace-pre-wrap">{url}</pre>
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<Headers headers={headers} type={'request'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Request;
|
||||
@@ -0,0 +1,43 @@
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
import Headers from "../Common/Headers/index";
|
||||
import Status from "../Common/Status/index";
|
||||
|
||||
const safeStringifyJSONIfNotString = (obj) => {
|
||||
if (obj === null || obj === undefined) return '';
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return '[Unserializable Object]';
|
||||
}
|
||||
};
|
||||
|
||||
const Response = ({ collection, response, item, width }) => {
|
||||
let { status, statusCode, statusText, dataBuffer, headers, data, error } = response || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Status */}
|
||||
<div className="mb-1">
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{response.duration && <span className="text-sm text-gray-400 ml-2">{response.duration}ms</span>}
|
||||
{response.size && <span className="text-sm text-gray-400 ml-2">{response.size}B</span>}
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<Headers headers={headers} type={'response'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Response;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user