mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
45 Commits
feat/file-
...
feat/oauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad5f0003c | ||
|
|
ee5e260890 | ||
|
|
c3f8959c54 | ||
|
|
cabd520ef1 | ||
|
|
0e7b0a0ce5 | ||
|
|
ccd4a14da6 | ||
|
|
98bd997665 | ||
|
|
a7cf24278e | ||
|
|
039c157f33 | ||
|
|
1009d42f92 | ||
|
|
1be0e8d31c | ||
|
|
ab9befd773 | ||
|
|
7506f83800 | ||
|
|
74d9b0aafe | ||
|
|
d3fcb42a8f | ||
|
|
3808089e60 | ||
|
|
cd2f5d5233 | ||
|
|
51be153527 | ||
|
|
5728b7c8a8 | ||
|
|
71b6907c31 | ||
|
|
eead96ca26 | ||
|
|
f99e8770f0 | ||
|
|
7ae33d05c9 | ||
|
|
ccb951dadd | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
4e88cbf318 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
b5e53ec25c | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 |
@@ -86,11 +86,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# run bruno-schema tests
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# run tests over all workspaces
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Requests
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing (পরীক্ষা)
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ব্রুনো-স্কিমা পরীক্ষা চালান
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Request (পুল অনুরোধ উত্থাপন)
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 运行 bruno-schema 测试
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 在所有工作区上运行测试
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# Führen Sie Bruno-Schema-Tests aus
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# Führen Sie Tests für alle Arbeitsbereiche durch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Pruebas
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ejecutar pruebas de esquema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# ejecutar pruebas en todos los espacios de trabajo
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crea un Pull Request
|
||||
|
||||
@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# exécuter des tests de schéma bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# exécuter des tests sur tous les espaces de travail
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### परिक्षण
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ब्रूनो-स्कीमा परीक्षण चलाएँ
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# सभी कार्यस्थानों पर परीक्षण चलाएँ
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### पुल अनुरोध प्रक्रिया
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# esegui i test dello schema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# esegui test su tutti gli spazi di lavoro
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### テストを動かすには
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ブルーノスキーマのテストを実行します
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# すべてのワークスペースでテストを実行します
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### プルリクエストの手順
|
||||
|
||||
@@ -66,11 +66,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 테스팅
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema 테스트 실행
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 모든 작업 공간에서 테스트 실행
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests 요청
|
||||
|
||||
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# voer bruno-schema tests uit
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# voer tests uit over alle werkruimten
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests indienen
|
||||
|
||||
@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testowanie
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# uruchom testy bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# uruchom testy we wszystkich przestrzeniach roboczych
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Tworzenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testando
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# executar testes do bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executar testes em todos os ambientes de trabalho
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Envio de Pull Request
|
||||
|
||||
@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testarea
|
||||
|
||||
```shell
|
||||
# bruno-schema
|
||||
# executați teste bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executați teste peste toate spațiile de lucru
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crearea unui Pull Request
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустите тесты bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустите тесты во всех рабочих пространствах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -67,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
# spustiť bruno-schema testy
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# spustiť testy vo všetkých pracovných priestoroch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema testlerini çalıştır
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# tüm çalışma alanlarında testleri çalıştır
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Request Oluşturma
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестування
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустити тести bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустити тести у всіх робочих просторах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 測試
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 執行布魯諾架構測試
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 對所有工作區執行測試
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 發送 Pull Request
|
||||
|
||||
630
package-lock.json
generated
630
package-lock.json
generated
@@ -18,6 +18,10 @@
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"dependencies": {
|
||||
"find-process": "^1.4.10",
|
||||
"pid-port": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
@@ -50,7 +54,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -1471,7 +1474,6 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@@ -1502,7 +1504,6 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1520,7 +1521,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
@@ -1800,7 +1800,6 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
||||
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.9",
|
||||
@@ -6831,6 +6830,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@@ -6851,6 +6856,18 @@
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/commons": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
|
||||
@@ -7767,7 +7784,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@@ -7780,7 +7796,6 @@
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@@ -7791,7 +7806,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
@@ -8291,6 +8305,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/address": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz",
|
||||
"integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
@@ -11028,7 +11051,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@@ -11266,7 +11288,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -11913,6 +11934,22 @@
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-port": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz",
|
||||
"integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"address": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"detect": "dist/commonjs/bin/detect-port.js",
|
||||
"detect-port": "dist/commonjs/bin/detect-port.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dev-null": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
|
||||
@@ -12639,7 +12676,6 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -13017,6 +13053,52 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-basic-auth": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
|
||||
@@ -13026,6 +13108,30 @@
|
||||
"basic-auth": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -13195,6 +13301,33 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/figures/node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/file/-/file-0.2.2.tgz",
|
||||
@@ -13296,6 +13429,45 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-process": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz",
|
||||
"integrity": "sha512-ncYFnWEIwL7PzmrK1yZtaccN8GhethD37RzBHG6iOZoFYB4vSmLLXfeWJjeN5nMvCJMjOtBvBBF8OgxEcikiZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "~4.1.2",
|
||||
"commander": "^12.1.0",
|
||||
"loglevel": "^1.9.2"
|
||||
},
|
||||
"bin": {
|
||||
"find-process": "bin/find-process.js"
|
||||
}
|
||||
},
|
||||
"node_modules/find-process/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-process/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
@@ -13607,7 +13779,6 @@
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -15076,6 +15247,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
@@ -15199,7 +15382,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
@@ -16943,6 +17125,19 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -17423,7 +17618,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
@@ -18636,6 +18830,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
@@ -18803,7 +19009,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -18849,6 +19054,12 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path/node_modules/inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
@@ -18971,6 +19182,155 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pid-port/-/pid-port-1.0.2.tgz",
|
||||
"integrity": "sha512-Khqp07zX8IJpmIg56bHrLxS3M0iSL4cq6wnMq8YE7r/hSw3Kn4QxYS6QJg8Bs22Z7CSVj7eSsxFuigYVIFWmjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"execa": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pid-port/node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
||||
@@ -20042,6 +20402,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
|
||||
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-quick": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.3.1.tgz",
|
||||
@@ -22393,7 +22768,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -22406,7 +22780,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -24167,7 +24540,7 @@
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -24239,6 +24612,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
|
||||
@@ -24755,7 +25140,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -25005,6 +25389,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
||||
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "0.32.11",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
|
||||
@@ -26293,11 +26689,15 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"content-disposition": "^0.5.4",
|
||||
"decomment": "^0.9.5",
|
||||
"detect-port": "^2.1.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-util": "^0.17.2",
|
||||
"execa": "^9.5.2",
|
||||
"express": "^4.21.2",
|
||||
"find-process": "^1.4.10",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
@@ -26309,6 +26709,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "3.3.8",
|
||||
"pid-port": "^1.0.2",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
@@ -27362,6 +27763,32 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/execa": {
|
||||
"version": "9.5.2",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
|
||||
"integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -27376,6 +27803,43 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/human-signals": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
|
||||
"integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -27394,6 +27858,58 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/npm-run-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-graphql-docs": {
|
||||
"name": "@usebruno/graphql-docs",
|
||||
"version": "0.1.0",
|
||||
@@ -27598,61 +28114,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/fast-xml-parser": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
|
||||
@@ -27671,27 +28132,6 @@
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-tests/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/strnum": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
|
||||
|
||||
@@ -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,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)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
folder={folder}
|
||||
/>
|
||||
{grantTypeComponentMap(collection, 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 (
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
.path-display {
|
||||
background: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-radius: 4px;
|
||||
@@ -23,14 +24,14 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
|
||||
.filename, .file-extension {
|
||||
.name-container, .file-extension {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.6;
|
||||
margin: 0 1px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,63 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconEdit, IconFolder, IconFile } from '@tabler/icons';
|
||||
import { IconFolder, IconFile } from '@tabler/icons';
|
||||
import path from 'utils/common/path';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const PathDisplay = ({
|
||||
collection,
|
||||
item,
|
||||
filename,
|
||||
extension = '.bru',
|
||||
showExtension = true,
|
||||
toggleEditingFilename,
|
||||
showDirectory = false
|
||||
baseName = '',
|
||||
iconType = 'file'
|
||||
}) => {
|
||||
const relativePath = item?.pathname && path.relative(collection?.pathname, showDirectory ? path.dirname(item?.pathname) : item?.pathname);
|
||||
const pathSegments = relativePath?.split(path.sep).filter(Boolean);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block font-medium">Location</label>
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="path-display">
|
||||
<div className="path-layout flex">
|
||||
<div className="icon-column flex">
|
||||
{showExtension ? <IconFile size={16} /> : <IconFolder size={16} />}
|
||||
</div>
|
||||
<div className="path-container flex font-mono items-center">
|
||||
<div className="path-segment collection-segment">
|
||||
{collection?.name}
|
||||
</div>
|
||||
|
||||
{pathSegments?.length > 0 && pathSegments?.map((segment, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<span className="separator">/</span>
|
||||
<div className="path-segment">
|
||||
{segment}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{collection && (
|
||||
<span className="separator">/</span>
|
||||
)}
|
||||
|
||||
<span className="filename">
|
||||
{filename}
|
||||
{showExtension && filename?.length ? (
|
||||
<span className="file-extension">{extension}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className="path-display mt-2">
|
||||
<div className="path-layout flex font-mono">
|
||||
<div className="icon-column flex">
|
||||
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
|
||||
</div>
|
||||
<span className="name-container">
|
||||
{baseName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -46,7 +46,7 @@ const Preferences = ({ onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
||||
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem]'>
|
||||
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2'>
|
||||
<div className="flex flex-col items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||
General
|
||||
|
||||
@@ -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,112 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef, useState, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, 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 { clearOauth2Cache, fetchOauth2Credentials, refreshOauth2Credentials } 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 { cloneDeep, find } from 'lodash';
|
||||
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index';
|
||||
import { BRUNO_OAUTH2_SERVER_CALLBACK_URL } from 'utils/common/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 [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
const [refreshingToken, toggleRefreshingToken] = useState(false);
|
||||
const [showRefreshButton, setShowRefreshButton] = useState(false);
|
||||
|
||||
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,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
authorizeInDefaultBrowser
|
||||
} = oAuth;
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
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 handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
folderUid: folder?.uid || null
|
||||
}));
|
||||
toggleFetchingToken(false);
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -40,7 +124,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
state,
|
||||
scope,
|
||||
pkce,
|
||||
[key]: value
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
authorizeInDefaultBrowser,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -61,6 +154,13 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
clientSecret,
|
||||
state,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
authorizeInDefaultBrowser,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
@@ -68,7 +168,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
@@ -77,16 +178,58 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const { uid: collectionUid } = collection;
|
||||
const interpolatedUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
useEffect(() => {
|
||||
// Update visibility whenever credentials change
|
||||
setShowRefreshButton(Boolean(creds?.refresh_token && creds?.access_token));
|
||||
}, [creds?.refresh_token, creds?.access_token, credentialsData]);
|
||||
|
||||
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>
|
||||
{/* Authorize in default browser */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(authorizeInDefaultBrowser)}
|
||||
onChange={(e) => handleChange('authorizeInDefaultBrowser', e.target.checked)}
|
||||
className={`cursor-pointer ml-1`}
|
||||
/>
|
||||
<label className={`block min-w-[140px]`}>Authorize In Default Browser</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">
|
||||
Use the default browser to authorize the user.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
let { key, label, isSecret } = input;
|
||||
let value = oAuth[key] || '';
|
||||
let shouldAuthorizeInDefaultBrowser = key == 'callbackUrl' && Boolean(authorizeInDefaultBrowser);
|
||||
if (shouldAuthorizeInDefaultBrowser) {
|
||||
value = BRUNO_OAUTH2_SERVER_CALLBACK_URL;
|
||||
}
|
||||
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] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -94,13 +237,39 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
readOnly={shouldAuthorizeInDefaultBrowser}
|
||||
/>
|
||||
</div>
|
||||
</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,10 +277,160 @@ 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
|
||||
<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.refreshUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshUrl", 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>
|
||||
<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>
|
||||
{showRefreshButton && (
|
||||
<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>
|
||||
)}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
@@ -120,4 +439,4 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
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,100 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef, useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, 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 { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } 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 toast from 'react-hot-toast';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/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 [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
const [refreshingToken, toggleRefreshingToken] = useState(false);
|
||||
|
||||
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,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleFetchingToken(false);
|
||||
toast.success('Token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
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,20 +108,48 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.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}
|
||||
@@ -62,9 +164,199 @@ 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.refreshUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
textarea {
|
||||
height: fit-content;
|
||||
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};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { readOauth2CachedCredentials } from 'utils/network';
|
||||
import { sendCollectionOauth2Request, sendRequest, clearOauth2Cache } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CredentialsPreview = ({ item, collection }) => {
|
||||
const oauth2CredentialsAreaRef = React.createRef();
|
||||
const [oauth2Credentials, setOauth2Credentials] = useState({});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
oauth2CredentialsAreaRef.current.value = oauth2Credentials;
|
||||
readOauth2CachedCredentials(collection.uid).then((credentials) => setOauth2Credentials(credentials));
|
||||
}, [oauth2CredentialsAreaRef]);
|
||||
|
||||
const handleRun = async () => {
|
||||
if (item) {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
} else {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: '' }))
|
||||
.then(() => {
|
||||
readOauth2CachedCredentials(collection.uid).then((credentials) => {
|
||||
setOauth2Credentials(credentials);
|
||||
toast.success('Cleared cache successfully');
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
const sortedFields = () => {
|
||||
const tokens = {};
|
||||
const extras = {};
|
||||
Object.entries(oauth2Credentials).forEach(([key, value]) => {
|
||||
if (key.endsWith('_token')) {
|
||||
tokens[key] = value;
|
||||
} else {
|
||||
extras[key] = value;
|
||||
}
|
||||
});
|
||||
return { ...tokens, ...extras };
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col w-full gap-1 mt-4">
|
||||
<div className="credential-item-wrapper" ref={oauth2CredentialsAreaRef}>
|
||||
{Object.entries(oauth2Credentials).length > 0 ? (
|
||||
<>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Access Token Cache
|
||||
</button>
|
||||
<details className="cursor-pointer flex flex-row w-full mt-2 gap-2">
|
||||
<summary>Cached OAuth2 Credentials</summary>
|
||||
{Object.entries(sortedFields()).map(([field, value]) => (
|
||||
<div key={field}>
|
||||
<label className="text-xs">{field}</label>
|
||||
<textarea className="w-full h-24 p-2 text-xs border rounded" value={value} readOnly />
|
||||
</div>
|
||||
))}
|
||||
</details>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialsPreview;
|
||||
@@ -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,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,162 @@
|
||||
import { find } from "lodash";
|
||||
import { interpolateStringUsingCollectionAndItem } from "utils/collections/index";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
|
||||
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 = interpolateStringUsingCollectionAndItem({ collection, item, string: 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 ? (
|
||||
<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,101 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, forwardRef, useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, 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 { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } 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 toast from 'react-hot-toast';
|
||||
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
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 [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
const [refreshingToken, toggleRefreshingToken] = useState(false);
|
||||
|
||||
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,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleFetchingToken(false);
|
||||
toast.success('Token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleSave = () => { save(); }
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
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,20 +111,48 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.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}
|
||||
@@ -64,11 +167,201 @@ 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.refreshUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</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, 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>
|
||||
|
||||
@@ -139,7 +139,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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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-lg 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-lg 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,46 @@
|
||||
const Network = ({ logs }) => {
|
||||
return (
|
||||
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{logs.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
</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,23 @@
|
||||
import Headers from "../Common/Headers/index";
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
|
||||
const Request = ({ collection, request, item, width }) => {
|
||||
const { url, headers, data, dataBuffer, error } = request || {};
|
||||
|
||||
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,26 @@
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
import Headers from "../Common/Headers/index";
|
||||
import Status from "../Common/Status/index";
|
||||
|
||||
const Response = ({ collection, response, item, width }) => {
|
||||
const { status, statusCode, statusText, headers, data, dataBuffer, error } = response || {};
|
||||
|
||||
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,79 @@
|
||||
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 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">
|
||||
<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;
|
||||
@@ -63,7 +63,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} />;
|
||||
|
||||
@@ -7,10 +7,10 @@ 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 RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
@@ -45,7 +45,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} />;
|
||||
|
||||
@@ -7,20 +7,22 @@ import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp } from "@tabler/icons";
|
||||
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionName: '',
|
||||
collectionFolderName: '',
|
||||
collectionName: `${name} copy`,
|
||||
collectionFolderName: `${sanitizeName(name)} copy`,
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
@@ -31,7 +33,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.test('is-valid-dir-name', function(value) {
|
||||
.test('is-valid-collection-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
@@ -92,7 +94,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -124,49 +126,70 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
<span
|
||||
className="text-link cursor-pointer hover:underline" onClick={browse}
|
||||
style={{
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
{isEditingFilename ?
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="flex items-center font-semibold">
|
||||
Folder Name
|
||||
<Help width="300">
|
||||
<p>
|
||||
The name of the folder used to store the collection.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
baseName={formik.values.collectionFolderName}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<PathDisplay
|
||||
filename={formik.values.collectionFolderName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
}
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.advanced-options {
|
||||
.caret {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
fill: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect, forwardRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
@@ -6,24 +6,32 @@ import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import * as path from 'path';
|
||||
import path from "utils/common/path";
|
||||
import { IconArrowBackUp, IconEdit, IconCaretDown } from "@tabler/icons";
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const itemName = item?.name;
|
||||
const itemType = item?.type;
|
||||
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
|
||||
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: itemName,
|
||||
filename: sanitizeName(itemFilename)
|
||||
name: `${itemName} copy`,
|
||||
filename: `${sanitizeName(itemName)} copy`
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
@@ -34,7 +42,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required')
|
||||
.test('is-valid-filename', function(value) {
|
||||
.test('is-valid-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
@@ -58,86 +66,157 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
const AdvancedOptions = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
|
||||
<button
|
||||
className="btn-advanced"
|
||||
type="button"
|
||||
>
|
||||
Options
|
||||
</button>
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Clone"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder='Enter Item name'
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
{isFolder ? 'Directory' : 'File'} Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
|
||||
handleCancel={onClose}
|
||||
hideFooter
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder='Enter Item name'
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
|
||||
{showFilesystemName && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="flex items-center font-semibold">
|
||||
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
|
||||
{ isFolder ? (
|
||||
<Help width="300">
|
||||
<p>
|
||||
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
|
||||
</p>
|
||||
</Help>
|
||||
) : (
|
||||
<Help width="300">
|
||||
<p>
|
||||
Bruno saves each request as a file in your collection's folder.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You can choose a file name different from your request's name or one compatible with filesystem rules.
|
||||
</p>
|
||||
</Help>
|
||||
)}
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder={isFolder ? 'Folder Name' : 'File Name'}
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
</div>
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
|
||||
<div className='flex advanced-options'>
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
key="show-filesystem-name"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
toggleShowFilesystemName(!showFilesystemName);
|
||||
}}
|
||||
>
|
||||
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<span className='mr-2'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
>
|
||||
Clone
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
showExtension={itemType !== 'folder'}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
showDirectory={true}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import * as path from 'path';
|
||||
import Help from 'components/Help';
|
||||
|
||||
const CollectionItemInfo = ({ item, onClose }) => {
|
||||
const { name, filename, type } = item;
|
||||
|
||||
const CollectionItemInfo = ({ collection, item, onClose }) => {
|
||||
const { pathname: collectionPathname } = collection;
|
||||
const { name, filename, pathname, type } = item;
|
||||
const relativePathname = path.relative(collectionPathname, pathname);
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
@@ -14,24 +13,43 @@ const CollectionItemInfo = ({ collection, item, onClose }) => {
|
||||
hideCancel={true}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="w-fit flex flex-col h-full">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">Name :</td>
|
||||
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>{name}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">{type=='folder' ? 'Directory Name' : 'File Name'} :</td>
|
||||
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>{filename}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right opacity-50">Pathname :</td>
|
||||
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={relativePathname}>{relativePathname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="w-fit flex flex-col h-full">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-left text-muted ">
|
||||
{type=='folder' ? 'Folder Name' : 'Request Name'}
|
||||
</td>
|
||||
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>
|
||||
<span className="mr-2">:</span>{name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-left text-muted flex items-center">
|
||||
{type == 'folder' ? 'Folder Name' : 'File Name'}
|
||||
<small className='font-normal text-muted ml-1'>(on filesystem)</small>
|
||||
{type == 'folder' ? (
|
||||
<Help width="300">
|
||||
<p>
|
||||
The name of the folder on your filesystem.
|
||||
</p>
|
||||
</Help>
|
||||
) : (
|
||||
<Help width="300">
|
||||
<p>
|
||||
Bruno saves each request as a file in your collection's folder.
|
||||
</p>
|
||||
</Help>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>
|
||||
<span className="mr-2">:</span>
|
||||
{filename}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.advanced-options {
|
||||
.caret {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
fill: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -6,20 +6,29 @@ import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import path from 'utils/common/path';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const itemName = item?.name;
|
||||
const itemType = item?.type;
|
||||
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
|
||||
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
@@ -35,7 +44,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.required('name is required')
|
||||
.test('is-valid-filename', function(value) {
|
||||
.test('is-valid-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
@@ -77,86 +86,157 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
const AdvancedOptions = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
|
||||
<button
|
||||
className="btn-advanced"
|
||||
type="button"
|
||||
>
|
||||
Options
|
||||
</button>
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={e => {e.preventDefault()}}>
|
||||
<div className='flex flex-col mt-2'>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
{isFolder ? 'Directory' : 'File'} Name
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
|
||||
handleCancel={onClose}
|
||||
hideFooter
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className='flex flex-col mt-2'>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('name', e.target.value);
|
||||
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
showExtension={itemType !== 'folder'}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
showDirectory={true}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{showFilesystemName && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="flex items-center font-semibold">
|
||||
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
|
||||
{ isFolder ? (
|
||||
<Help width="300">
|
||||
<p>
|
||||
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
|
||||
</p>
|
||||
</Help>
|
||||
) : (
|
||||
<Help width="300">
|
||||
<p>
|
||||
Bruno saves each request as a file in your collection's folder.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You can choose a file name different from your request's name or one compatible with filesystem rules.
|
||||
</p>
|
||||
</Help>
|
||||
)}
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder={isFolder ? 'Folder Name' : 'File Name'}
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
|
||||
<div className='flex advanced-options'>
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
key="show-filesystem-name"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
toggleShowFilesystemName(!showFilesystemName);
|
||||
}}
|
||||
>
|
||||
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<span className='mr-2'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import Modal from 'components/Modal';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import { IconArrowBackUp, IconEdit } from '@tabler/icons';
|
||||
import Help from 'components/Help';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -32,7 +32,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'must be 255 characters or less')
|
||||
.test('is-valid-dir-name', function(value) {
|
||||
.test('is-valid-collection-name', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
@@ -86,7 +86,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -105,7 +105,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
Bruno stores your collections on your computer's filesystem.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Choose where you want to store this collection.
|
||||
Choose the location where you want to store this collection.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
@@ -126,24 +126,46 @@ const CreateCollection = ({ onClose }) => {
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
<span
|
||||
className="text-link cursor-pointer hover:underline" onClick={browse}
|
||||
style={{
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
{isEditingFilename ?
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
{formik.values.collectionName?.trim()?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="flex items-center font-semibold">
|
||||
Folder Name
|
||||
<Help width="300">
|
||||
<p>
|
||||
The name of the folder used to store the collection.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
@@ -156,19 +178,18 @@ const CreateCollection = ({ onClose }) => {
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<PathDisplay
|
||||
filename={formik.values.collectionFolderName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
}
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
baseName={formik.values.collectionFolderName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.advanced-options {
|
||||
.caret {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
fill: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,18 +1,27 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import toast from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowBackUp } from '@tabler/icons';
|
||||
import { IconArrowBackUp, IconEdit} from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import PathDisplay from 'components/PathDisplay/index';
|
||||
import Help from 'components/Help';
|
||||
import Dropdown from "components/Dropdown";
|
||||
import { IconCaretDown } from "@tabler/icons";
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewFolder = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -58,80 +67,139 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
const AdvancedOptions = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
|
||||
<button
|
||||
className="btn-advanced"
|
||||
type="button"
|
||||
>
|
||||
Options
|
||||
</button>
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal size="md" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="folderName" className="block font-semibold">
|
||||
Folder Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
type="text"
|
||||
name="folderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('folderName', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.folderName || ''}
|
||||
/>
|
||||
{formik.touched.folderName && formik.errors.folderName ? (
|
||||
<div className="text-red-500">{formik.errors.folderName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="directoryName" className="block font-semibold">
|
||||
Directory Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Folder" hideFooter={true} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<label htmlFor="folderName" className="block font-semibold">
|
||||
Folder Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
type="text"
|
||||
name="folderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('folderName', e.target.value);
|
||||
!isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.folderName || ''}
|
||||
/>
|
||||
{formik.touched.folderName && formik.errors.folderName ? (
|
||||
<div className="text-red-500">{formik.errors.folderName}</div>
|
||||
) : null}
|
||||
|
||||
{showFilesystemName && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="directoryName" className="flex items-center font-semibold">
|
||||
Folder Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
|
||||
<Help width="300">
|
||||
<p>
|
||||
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
): (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="directoryName"
|
||||
placeholder="Folder Name"
|
||||
className={`block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.directoryName || ''}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
iconType="folder"
|
||||
baseName={formik.values.directoryName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formik.touched.directoryName && formik.errors.directoryName ? (
|
||||
<div className="text-red-500">{formik.errors.directoryName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
|
||||
<div className='flex advanced-options'>
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
key="show-filesystem-name"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
toggleShowFilesystemName(!showFilesystemName);
|
||||
}}
|
||||
>
|
||||
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<span className='mr-2'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="directoryName"
|
||||
placeholder="Directory Name"
|
||||
className={`block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.directoryName || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.directoryName}
|
||||
showExtension={false}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.directoryName && formik.errors.directoryName ? (
|
||||
<div className="text-red-500">{formik.errors.directoryName}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal>
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,45 +1,53 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.method-selector-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-right: none;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
.method-selector {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
div.method-selector-container,
|
||||
div.input-container {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
height: 2.3rem;
|
||||
}
|
||||
div.input-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
input {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const StyledWrapper = styled.div`
|
||||
div.method-selector-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-right: none;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
.method-selector {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
div.method-selector-container,
|
||||
div.input-container {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
height: 2.3rem;
|
||||
}
|
||||
div.input-container {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.border};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
input {
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StyledWrapper;
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-options {
|
||||
.caret {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
fill: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'rea
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import path from 'utils/common/path';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -11,10 +12,12 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { getRequestFromCurlCommand } from 'utils/curl';
|
||||
import { IconArrowBackUp, IconCaretDown } from '@tabler/icons';
|
||||
import { IconArrowBackUp, IconCaretDown, IconEdit } from '@tabler/icons';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import Portal from 'components/Portal';
|
||||
import Help from 'components/Help';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
@@ -24,10 +27,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
brunoConfig: { presets: collectionPresets = {} }
|
||||
} = collection;
|
||||
const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null);
|
||||
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const advancedDropdownTippyRef = useRef();
|
||||
const onAdvancedDropdownCreate = (ref) => (advancedDropdownTippyRef.current = ref);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
|
||||
@@ -57,7 +64,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
setCurlRequestTypeDetected(type);
|
||||
};
|
||||
|
||||
const [isEditingFilename, toggleEditingFilename] = useState(false);
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
|
||||
const getRequestType = (collectionPresets) => {
|
||||
if (!collectionPresets || !collectionPresets.requestType) {
|
||||
@@ -229,213 +236,279 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
const AdvancedOptions = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
|
||||
<button
|
||||
className="btn-advanced"
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
</label>
|
||||
Options
|
||||
</button>
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="http-request"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="http-request"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" hideFooter handleCancel={onClose}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={(event) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="graphql-request"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="from-curl"
|
||||
className="cursor-pointer ml-auto"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="from-curl"
|
||||
checked={formik.values.requestType === 'from-curl'}
|
||||
/>
|
||||
|
||||
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
|
||||
From cURL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
placeholder="Request Name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('requestName', e.target.value);
|
||||
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.requestName || ''}
|
||||
/>
|
||||
{formik.touched.requestName && formik.errors.requestName ? (
|
||||
<div className="text-red-500">{formik.errors.requestName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isEditingFilename ? (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="block font-semibold">
|
||||
File Name
|
||||
</label>
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditingFilename(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
id="http-request"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.filename || ''}
|
||||
value="http-request"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={(event) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="graphql-request"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="from-curl"
|
||||
className="cursor-pointer ml-auto"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="from-curl"
|
||||
checked={formik.values.requestType === 'from-curl'}
|
||||
/>
|
||||
|
||||
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
|
||||
From cURL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
item={item}
|
||||
filename={formik.values.filename}
|
||||
isEditingFilename={isEditingFilename}
|
||||
toggleEditingFilename={toggleEditingFilename}
|
||||
/>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
{formik.values.requestType !== 'from-curl' ? (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Request Name
|
||||
</label>
|
||||
<input
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
placeholder="Request Name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={e => {
|
||||
formik.setFieldValue('requestName', e.target.value);
|
||||
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
|
||||
}}
|
||||
value={formik.values.requestName || ''}
|
||||
/>
|
||||
{formik.touched.requestName && formik.errors.requestName ? (
|
||||
<div className="text-red-500">{formik.errors.requestName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{showFilesystemName && (
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
URL
|
||||
</label>
|
||||
<div className="flex items-center mt-2 ">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector
|
||||
method={formik.values.requestMethod}
|
||||
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="filename" className="flex items-center font-semibold">
|
||||
File Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
|
||||
<Help width="300">
|
||||
<p>
|
||||
Bruno saves each request as a file in your collection's folder.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You can choose a file name different from your request's name or one compatible with filesystem rules.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<IconArrowBackUp
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
) : (
|
||||
<IconEdit
|
||||
className="cursor-pointer opacity-50 hover:opacity-80"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => toggleEditing(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<input
|
||||
id="request-url"
|
||||
id="file-name"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
className="px-3 w-full "
|
||||
name="filename"
|
||||
placeholder="File Name"
|
||||
className={`!pr-10 block textbox mt-2 w-full`}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onPaste={handlePaste}
|
||||
value={formik.values.filename || ''}
|
||||
/>
|
||||
<span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, item?.pathname || collection?.pathname)}
|
||||
baseName={formik.values.filename? `${formik.values.filename}.bru` : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{formik.touched.requestUrl && formik.errors.requestUrl ? (
|
||||
<div className="text-red-500">{formik.errors.requestUrl}</div>
|
||||
)}
|
||||
{formik.touched.filename && formik.errors.filename ? (
|
||||
<div className="text-red-500">{formik.errors.filename}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
cURL Command
|
||||
</label>
|
||||
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('http-request');
|
||||
}}
|
||||
>
|
||||
HTTP
|
||||
)}
|
||||
{formik.values.requestType !== 'from-curl' ? (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
URL
|
||||
</label>
|
||||
<div className="flex items-center mt-2 ">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector
|
||||
method={formik.values.requestMethod}
|
||||
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
className="px-3 w-full "
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
{formik.touched.requestUrl && formik.errors.requestUrl ? (
|
||||
<div className="text-red-500">{formik.errors.requestUrl}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between">
|
||||
<label htmlFor="request-url" className="block font-semibold">
|
||||
cURL Command
|
||||
</label>
|
||||
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('http-request');
|
||||
}}
|
||||
>
|
||||
HTTP
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('graphql-request');
|
||||
}}
|
||||
>
|
||||
GraphQL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<textarea
|
||||
name="curlCommand"
|
||||
placeholder="Enter cURL request here.."
|
||||
className="block textbox w-full mt-4 curl-command"
|
||||
value={formik.values.curlCommand}
|
||||
onChange={handleCurlCommandChange}
|
||||
></textarea>
|
||||
{formik.touched.curlCommand && formik.errors.curlCommand ? (
|
||||
<div className="text-red-500">{formik.errors.curlCommand}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
|
||||
<div className='flex advanced-options'>
|
||||
<Dropdown onCreate={onAdvancedDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
curlRequestTypeChange('graphql-request');
|
||||
key="show-filesystem-name"
|
||||
onClick={(e) => {
|
||||
advancedDropdownTippyRef.current.hide();
|
||||
toggleShowFilesystemName(!showFilesystemName);
|
||||
}}
|
||||
>
|
||||
GraphQL
|
||||
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<textarea
|
||||
name="curlCommand"
|
||||
placeholder="Enter cURL request here.."
|
||||
className="block textbox w-full mt-4 curl-command"
|
||||
value={formik.values.curlCommand}
|
||||
onChange={handleCurlCommandChange}
|
||||
></textarea>
|
||||
{formik.touched.curlCommand && formik.errors.curlCommand ? (
|
||||
<div className="text-red-500">{formik.errors.curlCommand}</div>
|
||||
) : null}
|
||||
<div className='flex justify-end'>
|
||||
<span className='mr-2'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ const StyledWrapper = styled.div`
|
||||
height: 30px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
|
||||
@@ -53,6 +53,7 @@ class SingleLineEditor extends Component {
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
readOnly: this.props.readOnly,
|
||||
extraKeys: {
|
||||
Enter: runHandler,
|
||||
'Ctrl-Enter': runHandler,
|
||||
@@ -127,6 +128,9 @@ class SingleLineEditor extends Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
||||
this.editor.setOption('readOnly', this.props.readOnly);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
@@ -175,7 +179,7 @@ class SingleLineEditor extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full overflow-x-auto">
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||
<StyledWrapper ref={this.editorRef} className={`single-line-editor grow ${this.props.readOnly? 'disabled' : ''}`} />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const useIpcEvents = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -160,7 +161,17 @@ const useIpcEvents = () => {
|
||||
|
||||
const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
|
||||
dispatch(hydrateCollectionWithUiStateSnapshot(val));
|
||||
})
|
||||
});
|
||||
|
||||
const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {
|
||||
const payload = {
|
||||
...val,
|
||||
itemUid: val.itemUid || null,
|
||||
folderUid: val.folderUid || null,
|
||||
credentialsId: val.credentialsId || 'credentials'
|
||||
};
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeCollectionTreeUpdateListener();
|
||||
@@ -181,6 +192,7 @@ const useIpcEvents = () => {
|
||||
removeSystemProxyEnvUpdatesListener();
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig
|
||||
setCollectionSecurityConfig,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -237,11 +239,20 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
// Ensure any timestamps in the response are converted to numbers
|
||||
const serializedResponse = {
|
||||
...response,
|
||||
timeline: response.timeline?.map(entry => ({
|
||||
...entry,
|
||||
timestamp: entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp
|
||||
}))
|
||||
};
|
||||
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
response: response
|
||||
response: serializedResponse
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -799,7 +810,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
file: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1230,33 +1241,92 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
|
||||
|
||||
|
||||
export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {
|
||||
const collectionSnapshotData = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
try {
|
||||
if(!collectionSnapshotData) resolve();
|
||||
const { pathname, selectedEnvironment } = collectionSnapshotData;
|
||||
const collection = findCollectionByPathname(state.collections.collections, pathname);
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const collectionUid = collectionCopy?.uid;
|
||||
const collectionSnapshotData = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
try {
|
||||
if(!collectionSnapshotData) resolve();
|
||||
const { pathname, selectedEnvironment } = collectionSnapshotData;
|
||||
const collection = findCollectionByPathname(state.collections.collections, pathname);
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const collectionUid = collectionCopy?.uid;
|
||||
|
||||
// update selected environment
|
||||
if (selectedEnvironment) {
|
||||
const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
|
||||
if (environment) {
|
||||
dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
|
||||
}
|
||||
// update selected environment
|
||||
if (selectedEnvironment) {
|
||||
const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
|
||||
if (environment) {
|
||||
dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
|
||||
}
|
||||
}
|
||||
|
||||
// todo: add any other redux state that you want to save
|
||||
|
||||
// todo: add any other redux state that you want to save
|
||||
|
||||
resolve();
|
||||
}
|
||||
catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, itemUid, folderUid } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
|
||||
.then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {
|
||||
dispatch(
|
||||
collectionAddOauth2CredentialsByUrl({
|
||||
credentials,
|
||||
url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: folderUid || null,
|
||||
itemUid: !folderUid ? itemUid : null
|
||||
})
|
||||
);
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, folderUid, itemId } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
|
||||
.then(({ credentials, url, collectionUid, debugInfo }) => {
|
||||
dispatch(
|
||||
collectionAddOauth2CredentialsByUrl({
|
||||
credentials,
|
||||
url,
|
||||
collectionUid,
|
||||
debugInfo,
|
||||
folderUid: folderUid || null,
|
||||
itemId: !folderUid ? itemId : null
|
||||
})
|
||||
);
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
|
||||
const { collectionUid, url, credentialsId } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
|
||||
.then(() => {
|
||||
// We do not dispatch any action to modify the Redux store,
|
||||
// since we are only clearing the session on the main process side.
|
||||
resolve();
|
||||
}
|
||||
catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
@@ -281,13 +281,38 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
// Ensure timestamp is a number (milliseconds since epoch)
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
|
||||
console.log("response reieved", JSON.stringify(item), JSON.stringify(item.requestSent));
|
||||
|
||||
// Append the new timeline entry with numeric timestamp
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
itemUid: item.uid,
|
||||
timestamp: timestamp,
|
||||
data: {
|
||||
request: item.requestSent || item.request,
|
||||
response: action.payload.response,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1539,6 +1564,26 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateFolderAuth: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
console.log('action.payload.content inside bro', action.payload);
|
||||
if (!collection) return;
|
||||
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
|
||||
console.log('folder inside bro', folder);
|
||||
if (!folder) return;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
switch (action.payload.mode) {
|
||||
case 'oauth2':
|
||||
set(folder, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
addCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2019,7 +2064,85 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.docs', action.payload.docs);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
// Update oauth2Credentials (latest token)
|
||||
if (!collection.oauth2Credentials) {
|
||||
collection.oauth2Credentials = [];
|
||||
}
|
||||
let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);
|
||||
|
||||
// Remove existing credentials for the same combination
|
||||
const filteredOauth2Credentials = filter(
|
||||
collectionOauth2Credentials,
|
||||
(creds) =>
|
||||
!(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)
|
||||
);
|
||||
|
||||
// Add the new credential with folderUid and itemUid
|
||||
filteredOauth2Credentials.push({
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
});
|
||||
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
if(debugInfo) {
|
||||
collection.timeline.push({
|
||||
type: "oauth2",
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo: debugInfo.data,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
collectionClearOauth2CredentialsByUrl: (state, action) => {
|
||||
// Since we don't want to remove tokens from oauth2Credentials or timeline,
|
||||
},
|
||||
|
||||
collectionGetOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
const oauth2Credential = find(
|
||||
collection?.oauth2Credentials || [],
|
||||
(creds) =>
|
||||
creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2124,6 +2247,11 @@ export const {
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
moveCollection
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ body {
|
||||
font-kerning: none;
|
||||
text-rendering: optimizeSpeed;
|
||||
letter-spacing: normal;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'utils/common/path';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -381,11 +383,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
di.request.auth.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
|
||||
username: get(si.request, 'auth.oauth2.username', ''),
|
||||
password: get(si.request, 'auth.oauth2.password', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', '')
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -394,19 +404,36 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
|
||||
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
pkce: get(si.request, 'auth.oauth2.pkce', false)
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
pkce: get(si.request, 'auth.oauth2.pkce', false),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
authorizeInDefaultBrowser: get(si.request, 'auth.oauth2.authorizeInDefaultBrowser', ''),
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
di.request.auth.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', '')
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -953,12 +980,15 @@ export const getAllVariables = (collection, item) => {
|
||||
|
||||
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
|
||||
|
||||
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials })
|
||||
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
@@ -1025,4 +1055,44 @@ const mergeVars = (collection, requestTreePath = []) => {
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const interpolateStringUsingCollectionAndItem = ({ collection, item, string }) => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
const value = interpolate(string, variables);
|
||||
return value;
|
||||
}
|
||||
|
||||
export const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {
|
||||
__name__: environment.name
|
||||
};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...envVars,
|
||||
__name__: environment.name
|
||||
};
|
||||
};
|
||||
|
||||
export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
||||
let credentialsVariables = {};
|
||||
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
||||
if (credentials) {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
return credentialsVariables;
|
||||
};
|
||||
|
||||
@@ -187,6 +187,8 @@ export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
|
||||
|
||||
export const getEncoding = (headers) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers['content-type'] || '');
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || '');
|
||||
return charsetMatch?.[1];
|
||||
}
|
||||
|
||||
export const BRUNO_OAUTH2_SERVER_CALLBACK_URL = `http://localhost:9876/callback`;
|
||||
@@ -2,7 +2,7 @@ const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid character
|
||||
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
|
||||
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
|
||||
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
|
||||
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
|
||||
const lastCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot or space at end, hyphen allowed
|
||||
|
||||
export const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -29,6 +29,7 @@ export const validateName = (name) => {
|
||||
|
||||
export const validateNameError = (name) => {
|
||||
if (!name) return "Name cannot be empty.";
|
||||
|
||||
if (name.length > 255) {
|
||||
return "Name cannot exceed 255 characters.";
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import parseCurlCommand from './parse-curl';
|
||||
import * as querystring from 'query-string';
|
||||
import * as jsesc from 'jsesc';
|
||||
import * as path from 'path';
|
||||
|
||||
function getContentType(headers = {}) {
|
||||
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||
import each from 'lodash/each';
|
||||
import { BRUNO_OAUTH2_SERVER_CALLBACK_URL } from 'utils/common/index';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -431,6 +432,67 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
|
||||
placement: "header" //By default we are placing the apikey values in headers!
|
||||
}
|
||||
} else if (auth.type === 'oauth2'){
|
||||
const findValueUsingKey = (key) => {
|
||||
return auth?.oauth2?.find(v => v?.key == key)?.value ?? ''
|
||||
}
|
||||
const oauth2GrantTypeMaps = {
|
||||
'authorization_code_with_pkce': 'authorization_code',
|
||||
'authorization_code': 'authorization_code',
|
||||
'client_credentials': 'client_credentials',
|
||||
'password_credentials': 'password_credentials'
|
||||
}
|
||||
const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code';
|
||||
if (grantType) {
|
||||
brunoRequestItem.request.auth.mode = 'oauth2';
|
||||
switch(grantType) {
|
||||
case 'authorization_code':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'authorization_code',
|
||||
authorizationUrl: findValueUsingKey('authUrl'),
|
||||
callbackUrl: Boolean(findValueUsingKey('useBrowser')) ? BRUNO_OAUTH2_SERVER_CALLBACK_URL : findValueUsingKey('redirect_uri'),
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header',
|
||||
authorizeInDefaultBrowser: Boolean(findValueUsingKey('useBrowser')),
|
||||
};
|
||||
break;
|
||||
case 'password_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
username: findValueUsingKey('username'),
|
||||
password: findValueUsingKey('password'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
size: response.size,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
@@ -36,17 +37,14 @@ const sendHttpRequest = async (item, collection, environment, runtimeVariables)
|
||||
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
resolve({});
|
||||
});
|
||||
};
|
||||
|
||||
export const clearOauth2Cache = async (uid) => {
|
||||
export const readOauth2CachedCredentials = async (uid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('read-oauth2-cached-credentials', uid).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -147,13 +147,13 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
// todo: we have things happening in two places w.r.t basic auth
|
||||
// need to refactor this in the future
|
||||
// the request.auth (basic auth) object gets set inside the prepare-request.js file
|
||||
if (request.auth) {
|
||||
const username = _interpolate(request.auth.username) || '';
|
||||
const password = _interpolate(request.auth.password) || '';
|
||||
if (request.basicAuth) {
|
||||
const username = _interpolate(request.basicAuth.username) || '';
|
||||
const password = _interpolate(request.basicAuth.password) || '';
|
||||
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.auth;
|
||||
delete request.basicAuth;
|
||||
}
|
||||
|
||||
if (request.awsv4config) {
|
||||
@@ -165,12 +165,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.awsv4config.profileName = _interpolate(request.awsv4config.profileName) || '';
|
||||
}
|
||||
|
||||
// interpolate vars for ntlmConfig auth
|
||||
if (request.ntlmConfig) {
|
||||
request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';
|
||||
request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';
|
||||
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
|
||||
}
|
||||
// interpolate vars for ntlmConfig auth
|
||||
if (request.ntlmConfig) {
|
||||
request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';
|
||||
request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';
|
||||
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
|
||||
}
|
||||
|
||||
if(request?.auth) delete request.auth;
|
||||
|
||||
if (request) return request;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
if (collectionAuth && request.auth?.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
axiosRequest.basicAuth = {
|
||||
username: get(collectionAuth, 'basic.username'),
|
||||
password: get(collectionAuth, 'basic.password')
|
||||
};
|
||||
@@ -69,7 +69,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
if (request.auth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
axiosRequest.basicAuth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
const expected = { username: 'testUser', password: 'testPass123' };
|
||||
expect(result.auth).toEqual(expected);
|
||||
expect(result.basicAuth).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-util": "^0.17.2",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
|
||||
176
packages/bruno-electron/src/app/about-bruno.js
Normal file
176
packages/bruno-electron/src/app/about-bruno.js
Normal file
@@ -0,0 +1,176 @@
|
||||
module.exports = function aboutBruno({version}) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
|
||||
<title>About Bruno</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
}
|
||||
.logo {
|
||||
margin-top: 0px;
|
||||
}
|
||||
.title {
|
||||
font-size: 24px;
|
||||
margin-top: 5px;
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
}
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.buttons {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
.link {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 10px 15px;
|
||||
background-color: #F4AA41;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.link:hover {
|
||||
background-color: #F4AA41;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo">
|
||||
</div>
|
||||
<svg id="emoji" width="100" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="color">
|
||||
<path
|
||||
fill="#F4AA41"
|
||||
stroke="none"
|
||||
d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"
|
||||
/>
|
||||
<polygon
|
||||
fill="#EA5A47"
|
||||
stroke="none"
|
||||
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
|
||||
/>
|
||||
<polygon
|
||||
fill="#3F3F3F"
|
||||
stroke="none"
|
||||
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
|
||||
/>
|
||||
</g>
|
||||
<g id="hair" />
|
||||
<g id="skin" />
|
||||
<g id="skin-shadow" />
|
||||
<g id="line">
|
||||
<path
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"
|
||||
/>
|
||||
<path
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
|
||||
/>
|
||||
<line
|
||||
x1="36.2078"
|
||||
x2="36.2078"
|
||||
y1="47.3393"
|
||||
y2="44.3093"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<h2 class="title">Bruno ${version}</h2>
|
||||
<footer class="footer">
|
||||
©2025 Bruno Software Inc
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
const { ipcMain } = require('electron');
|
||||
const os = require('os');
|
||||
const openAboutWindow = require('about-window').default;
|
||||
const { join } = require('path');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { version } = require('../../package.json');
|
||||
const aboutBruno = require('./about-bruno');
|
||||
|
||||
const template = [
|
||||
{
|
||||
@@ -77,14 +78,16 @@ const template = [
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Bruno',
|
||||
click: () =>
|
||||
openAboutWindow({
|
||||
product_name: 'Bruno',
|
||||
icon_path: join(__dirname, '../about/256x256.png'),
|
||||
css_path: join(__dirname, '../about/about.css'),
|
||||
homepage: 'https://www.usebruno.com/',
|
||||
package_json_dir: join(__dirname, '../..')
|
||||
})
|
||||
click: () => {
|
||||
const aboutWindow = new BrowserWindow({
|
||||
width: 350,
|
||||
height: 250,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
});
|
||||
aboutWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(aboutBruno({version}))}`);
|
||||
}
|
||||
},
|
||||
{ label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }
|
||||
]
|
||||
|
||||
@@ -54,6 +54,7 @@ const jsonToCollectionBru = async (json, isFolder) => {
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
@@ -66,10 +67,6 @@ const jsonToCollectionBru = async (json, isFolder) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isFolder) {
|
||||
collectionBruJson.auth = _.get(json, 'request.auth', {});
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -31,7 +31,7 @@ const lastOpenedCollections = new LastOpenedCollections();
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
"connect-src 'self' https://*.posthog.com",
|
||||
"font-src 'self' https:",
|
||||
"font-src 'self' https: data:;",
|
||||
"frame-src data:",
|
||||
// this has been commented out to make oauth2 work
|
||||
// "form-action 'none'",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user