mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
205 Commits
feat/oauth
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942c0ee113 | ||
|
|
dbf8af1146 | ||
|
|
d7ccf1454e | ||
|
|
652d447f8b | ||
|
|
2f58379feb | ||
|
|
d4673a2f07 | ||
|
|
3a0c94577f | ||
|
|
c407b73c22 | ||
|
|
361add3385 | ||
|
|
9d6ab69d37 | ||
|
|
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 | ||
|
|
38c307d6f1 | ||
|
|
520567793a | ||
|
|
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 | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
4e88cbf318 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
b5e53ec25c | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 | ||
|
|
f972733426 |
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
|
||||
|
||||
44
.github/workflows/playwright.yml
vendored
Normal file
44
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Playwright E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
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
|
||||
9
.github/workflows/tests.yml
vendored
9
.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: |
|
||||
|
||||
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,67 @@ 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
|
||||
```
|
||||
|
||||
### 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 +116,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/test-app-start.spec.ts
Normal file
5
e2e-tests/test-app-start.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../playwright';
|
||||
|
||||
test('test-app-start', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
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
30
package.json
30
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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -13,4 +13,4 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,86 @@
|
||||
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';
|
||||
|
||||
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 handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
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,62 @@
|
||||
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('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,14 +10,51 @@ import NTLMAuth from './NTLMAuth';
|
||||
|
||||
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);
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
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) {
|
||||
@@ -46,32 +83,21 @@ const Auth = ({ item, collection }) => {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -10,10 +10,10 @@ import QueryResultPreview from './QueryResultPreview';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, prettifyJson, uuid } from 'utils/common/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 '';
|
||||
}
|
||||
|
||||
@@ -37,15 +37,12 @@ const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (filter) {
|
||||
try {
|
||||
data = JSONPath({ path: filter, json: data });
|
||||
return prettifyJson(JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.warn('Could not apply JSONPath filter:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Prettify the JSON string directly instead of parse->stringify to avoid
|
||||
// issues like rounding numbers bigger than Number.MAX_SAFE_INTEGER etc.
|
||||
return prettifyJson(rawData);
|
||||
return safeStringifyJSON(data, true);
|
||||
}
|
||||
|
||||
if (mode.includes('xml')) {
|
||||
@@ -60,7 +57,7 @@ const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
return data;
|
||||
}
|
||||
|
||||
return prettifyJson(rawData);
|
||||
return safeStringifyJSON(data, true);
|
||||
};
|
||||
|
||||
const formatErrorMessage = (error) => {
|
||||
@@ -68,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;
|
||||
}
|
||||
@@ -80,7 +77,11 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
const formattedData = useMemo(
|
||||
() => formatResponse(data, dataBuffer, responseEncoding, mode, filter),
|
||||
[data, dataBuffer, responseEncoding, mode, filter]
|
||||
);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
@@ -91,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() });
|
||||
@@ -139,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;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import Network from "./Network/index";
|
||||
import Request from "./Request/index";
|
||||
import Response from "./Response/index";
|
||||
import Method from "./Common/Method/index";
|
||||
import Status from "./Common/Status/index";
|
||||
import { RelativeTime } from "./Common/Time/index";
|
||||
|
||||
const TimelineItem = ({ timestamp, request, response, item, collection, width, isOauth2 }) => {
|
||||
const [isCollapsed, _toggleCollapse] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('request');
|
||||
const toggleCollapse = () => _toggleCollapse(prev => !prev);
|
||||
const { method, status, statusCode, statusText, url = '' } = request || {};
|
||||
const { status: responseStatus, statusCode: responseStatusCode, statusText: responseStatusText } = response || {};
|
||||
const showNetworkLogs = response.timeline && response.timeline.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50' } py-2`}>
|
||||
<div className="oauth-request-item-header cursor-pointer" onClick={toggleCollapse}>
|
||||
<div className="flex justify-between items-center min-w-0">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />
|
||||
<Method method={method} />
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{isOauth2 ? <pre className="opacity-50">[oauth2.0]</pre> : null}
|
||||
<pre className="opacity-70">[{new Date(timestamp).toISOString()}]</pre>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<RelativeTime timestamp={timestamp} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate text-sm mt-1">{url}</div>
|
||||
</div>
|
||||
{isCollapsed && (<div className="text-sm overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="tabs-switcher flex mb-4">
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('request')}
|
||||
>
|
||||
Request
|
||||
</button>
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('response')}
|
||||
>
|
||||
Response
|
||||
</button>
|
||||
{showNetworkLogs && (
|
||||
<button
|
||||
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('networkLogs')}
|
||||
>
|
||||
Network Logs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content">
|
||||
{/* Request Tab */}
|
||||
{activeTab === 'request' && (
|
||||
<Request request={request} item={item} collection={collection} width={width} />
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<Response response={response} item={item} collection={collection} width={width} />
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
{activeTab === 'networkLogs' && showNetworkLogs && (
|
||||
<Network logs={response?.timeline} />
|
||||
)}
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
||||
@@ -1,61 +1,123 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import React, { useState } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
import { get } from 'lodash';
|
||||
import TimelineItem from './TimelineItem/index';
|
||||
|
||||
const Timeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
uid: collection.uid,
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
// Get path from collection to item
|
||||
let path = [];
|
||||
let currentItem = findItemInCollection(collection, item?.uid);
|
||||
while (currentItem) {
|
||||
path.unshift(currentItem);
|
||||
currentItem = findParentItemInCollection(collection, currentItem?.uid);
|
||||
}
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...path].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',
|
||||
uid: i.uid,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const Timeline = ({ collection, item, width }) => {
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
|
||||
// Filter timeline entries based on new rules
|
||||
const combinedTimeline = ([...(collection.timeline || [])]).filter(obj => {
|
||||
// Always show entries for this item
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
|
||||
// For OAuth2 entries, also show if auth is inherited
|
||||
if (obj.type === 'oauth2' && authSource) {
|
||||
if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;
|
||||
if (authSource.type === 'collection' && !obj.folderUid) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full flex flex-grow flex-col"
|
||||
style={{ maxWidth: width - 60, overflowWrap: 'break-word' }}
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
if (event.type === 'request') {
|
||||
const { data, timestamp } = event;
|
||||
const { request, response } = data;
|
||||
return (
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{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) => {
|
||||
} else if (event.type === 'oauth2') {
|
||||
const { data, timestamp } = event;
|
||||
const { debugInfo } = data;
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
<div key={index} className="timeline-event">
|
||||
<div className="timeline-event-header cursor-pointer flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="font-bold">OAuth2.0 Calls</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{debugInfo && debugInfo.length > 0 ? (
|
||||
debugInfo.map((data, idx) => (
|
||||
<div className='ml-4'>
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
timestamp={timestamp}
|
||||
request={data?.request}
|
||||
response={data?.response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width - 50}
|
||||
isOauth2={true}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No debug information available.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
export default Timeline;
|
||||
@@ -18,6 +18,8 @@ import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -26,6 +28,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
|
||||
|
||||
const requestTimeline = ([...(collection.timeline || [])]).filter(obj => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
@@ -42,6 +48,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
const responseSize = response.size || 0;
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -63,7 +70,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={item.requestSent} response={item.response} />;
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
@@ -75,6 +82,14 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (item.response && item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
@@ -83,7 +98,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!item.response) {
|
||||
if (!item.response && !requestTimeline?.length) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Placeholder />
|
||||
@@ -134,11 +149,17 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={response.size} />
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
<>
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -152,7 +173,17 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{getTabPanel(focusedTab.responsePaneTab)}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
width={rightPaneWidth}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,10 @@ const StyledWrapper = styled.div`
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -7,10 +7,11 @@ import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SkippedRequest from 'components/ResponsePane/SkippedRequest';
|
||||
import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
@@ -45,7 +46,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
return <RunnerTimeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
@@ -63,6 +64,14 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
|
||||
@@ -39,6 +39,10 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
@@ -102,6 +102,9 @@ export default function RunnerResults({ collection }) {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
});
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
@@ -159,7 +162,8 @@ export default function RunnerResults({ collection }) {
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
</div>
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
@@ -172,14 +176,18 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== 'error' && item.testStatus === 'pass' && item.status !== 'skipped' ? (
|
||||
{item.testStatus === 'pass' && item.assertionStatus === 'pass' ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
@@ -263,11 +271,15 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
{selectedItem.testStatus === 'pass' && selectedItem.assertionStatus === 'pass' ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
: null}
|
||||
{selectedItem.status === 'error' || selectedItem.testStatus === 'fail' || selectedItem.assertionStatus === 'fail' ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -7,8 +7,11 @@ import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const ShareCollection = ({ onClose, collection }) => {
|
||||
const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const handleExportBrunoCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -11,11 +11,13 @@ import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
const CloneCollection = ({ onClose, collectionUid }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
values.collectionName,
|
||||
values.collectionFolderName,
|
||||
values.collectionLocation,
|
||||
collection.pathname
|
||||
collection?.pathname
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const CloneCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Request cloned!');
|
||||
onClose();
|
||||
@@ -172,8 +172,6 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.drag-preview {
|
||||
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
import {
|
||||
IconFile,
|
||||
IconFolder,
|
||||
} from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
function getItemStyles({ x, y }) {
|
||||
if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' };
|
||||
const transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
top: 0,
|
||||
transform,
|
||||
WebkitTransform: transform,
|
||||
zIndex: 100,
|
||||
};
|
||||
}
|
||||
|
||||
export const CollectionItemDragPreview = () => {
|
||||
const {
|
||||
item,
|
||||
isDragging,
|
||||
clientOffset
|
||||
} = useDragLayer((monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
isDragging: monitor.isDragging(),
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
}));
|
||||
if (!isDragging) return null;
|
||||
const { x, y } = clientOffset || {};
|
||||
const shouldShowFolderIcon = !item.type || item.type === 'folder';
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div style={getItemStyles({ x, y })} className='p-2'>
|
||||
<div className='flex items-center gap-2 border border-gray-500/10 rounded-md px-2 py-1 drag-preview'>
|
||||
{shouldShowFolderIcon ? (
|
||||
<IconFolder size={16} />
|
||||
) : (
|
||||
<IconFile size={16} />
|
||||
)}
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { recursivelyGetAllItemUids } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollectionItem = ({ onClose, item, collection }) => {
|
||||
const DeleteCollectionItem = ({ onClose, item, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
|
||||
dispatch(deleteItem(item.uid, collectionUid)).then(() => {
|
||||
|
||||
if (isFolder) {
|
||||
// close all tabs that belong to the folder
|
||||
|
||||
@@ -62,6 +62,7 @@ const CodeView = ({ language, item }) => {
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
item={item}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
|
||||
@@ -10,9 +10,11 @@ import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
return;
|
||||
}
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
await dispatch(saveRequest(item.uid, collectionUid, true));
|
||||
}
|
||||
const { name: newName, filename: newFilename } = values;
|
||||
try {
|
||||
let renameConfig = {
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid,
|
||||
};
|
||||
renameConfig['newName'] = newName;
|
||||
if (itemFilename !== newFilename) {
|
||||
@@ -191,8 +191,6 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user