Compare commits

...

112 Commits

Author SHA1 Message Date
Anoop M D
f8ba781340 chore: version bump 2024-03-21 00:50:03 +05:30
Baptiste Poulain
f96f763f14 fix(enableTranslation): remove unused enableTranslation and useTranslation tokens (#1867)
Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
2024-03-20 18:45:27 +05:30
Afrian Junior
a546457e0f fix(ux): better text selection implementation on dark-mode (#1861) 2024-03-20 15:52:44 +05:30
Feldrise
2b0ad29b93 fix: system theme in dark mode (#1823) 2024-03-19 13:11:09 +05:30
Anoop M D
cdbb15f33e chore: bumped version to v1.12.0 2024-03-19 06:36:38 +05:30
Anoop M D
14911b4def chore: removed dependency on tailwind forms 2024-03-19 06:34:54 +05:30
Baptiste Poulain
410eecc884 feature(postman_tests_scripts): automatic tests and scripts translation from postman import (#1151)
* feature(postman_tests_scripts): automatic tests and scripts translation from postman import
---------

Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local>
Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
2024-03-13 18:40:31 +05:30
Anoop M D
2cd0e065bd chore: updated lib versions 2024-03-13 03:05:29 +05:30
Anoop M D
d0c7c872c9 chore: bumped version to v1.11.0 2024-03-13 00:52:00 +05:30
Anoop M D
63684afbff chore: bruno notifications endpoint 2024-03-13 00:31:46 +05:30
Anoop M D
13eef748e1 chore: updated package lock 2024-03-12 23:56:10 +05:30
Anoop M D
dbe41e7f59 fix(#1731): fix aws sdk issue 2024-03-12 23:53:14 +05:30
Anoop M D
c00bfb0ce4 fix: fixed failing tests 2024-03-12 23:47:08 +05:30
Anoop M D
09aefffc47 fix: fixed failing tests 2024-03-12 23:43:50 +05:30
James Hall
6629d5a2c8 fix(#1521): only show Recent Documents menu on supporting platforms. (#1585)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-03-12 23:33:30 +05:30
Scott LaPlante
3ee76067fb CLI fixes for aws and environment modifications (#1713)
* Interpolate awsv4 values to support them including templated values.

Closes #1508

* change to let to allow for rewrite; rename to envVariables for consistency

* When running via CLI, preserve changes to collection variables and
environment variables.

Closes #1255

* Closes #1255 - set well known variable name on environment

* Revert "When running via CLI, preserve changes to collection variables and"

This reverts commit 7c94c9ec19.

* Revert "change to let to allow for rewrite; rename to envVariables for consistency"

This reverts commit 9320b8faf0.

---------

Co-authored-by: Scott LaPlante <scott.laplante@flueid.com>
2024-03-12 23:20:37 +05:30
patest-dev
e6090a4d59 Update the german readme file (#1759)
* Update readme_de.md

* Update readme_de.md
2024-03-12 23:19:15 +05:30
Gabriel
c257603e17 docs: update pt-br readme (#1760) 2024-03-12 23:18:24 +05:30
patest-dev
ee441d2ab6 Fix bruno-cli readme changelog link (#1770) 2024-03-12 23:17:47 +05:30
Anoop M D
6a2754d4fb feat: refactor and improve notifications implementation 2024-03-12 02:50:06 +05:30
lohit
b0f4491cd2 feat(#BRU-31): notifications feature draft (#1730)
* feat(#BRU-31): notifications feature
* feat(#BRU-31): date correction
2024-03-11 17:48:52 +05:30
Grant Forsythe
1fca217046 fix: broken link in readme (#1737)
* fix: broken link in readme
* fix: replace relative link with an absolute link
2024-03-11 17:46:14 +05:30
trusta
5ec2475f31 feat(#1659): add html reporter for cli (#1660) 2024-03-11 02:13:25 +05:30
Isaac Hatton
f1b80ba0ff contributing.md Capitalisation corrections (#1682)
Make Capitalisation of Bruno consistent with README.md and also make Pull Request plural to fit better gramatically.
2024-03-11 02:09:44 +05:30
Warren Buckley
475b585fdd Updates readme with winget install command (#1670) 2024-03-11 02:08:45 +05:30
lohit
95b59b06e8 feat(#BRU-26): json filter button with expandable input bar (#1699)
* feat(#BRU-26): JSON filter UI
* feat(#BRU-22): prettify graphql toast
2024-03-11 02:06:27 +05:30
lohit
6a05321109 feat(#1003): closing stale 'authorize' windows | handling error, error_description, error_uri query params for oauth2 | clear authorize window cache for authorization_code oauth2 flow (#1719)
* feat(#1003): oauth2 support
---------

Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-03-11 01:51:55 +05:30
Daniel Subiabre García
86ddd2b9b0 Update readme_es with new content and typo fixes (#1723) 2024-03-11 01:49:00 +05:30
Julien Ma
80142dbfcd Fix margin between items in Welcome > Links (#1742) 2024-03-11 01:44:49 +05:30
Anoop M D
3683d4c1df Merge pull request #1746 from shuuji3/patch-1
docs(readme.md): fix broken CI badge
2024-03-11 01:40:39 +05:30
TAKAHASHI Shuuji
3ef8135173 docs(readme.md): fix broken CI badge 2024-03-11 02:35:42 +09:00
Anoop M D
e7dacde46a chore: added sponsors section 2024-03-07 21:22:27 +05:30
Mateusz Pietryga
5f35d71b8b Fix #1683 allow OAuth2 authorizationUrl with user provided query parameters (#1712) 2024-03-04 17:21:12 +05:30
Sanjai Kumar
e2d1f52993 Fix/json with bigints (#1710)
* fix(#1689): JSON with Bigints support
* added Jsonbigint support for cli
2024-03-04 15:32:35 +05:30
Jack Jiang
cc02794ce9 Use URL Encoded Form for OAuth2.0 token endpoint (#1701)
OAuth2.0 expects URL encoded form instead of JSON content
https://www.oauth.com/oauth2-servers/server-side-apps/example-flow/

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-03-04 15:22:34 +05:30
lohit
858536e13d feat(#1003): collection level oauth2, access_token_url & scope for 'Client Credentials' and 'Password Credentials' grant types (#1691)
* feat(#1003): authorization_code grant type PKCE support, code cleanup..
---------

Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-03-04 15:21:05 +05:30
Anoop M D
9d3df0a86a chore: update readme 2024-02-29 21:44:06 +05:30
Anoop M D
0e85b302b8 chore: bumped version to v1.10.0 2024-02-28 00:40:10 +05:30
Anoop M D
3b51621580 chore: updated golden edition org pricing 2024-02-28 00:23:44 +05:30
Gabriel
18e7301550 feat: add middle mouse button click to close tab (#1649)
* feat: add middle click button to close tab
* refactor: remove unused code
* fix: verify if is middle click before trigger close confirmation modal
2024-02-27 23:58:01 +05:30
Anoop M D
96bcc7074a chore: prettify icon styling updates 2024-02-27 23:48:03 +05:30
lohit
f64e13a71f feat(#bru-22): prettify graphql placement and styling (#1675) 2024-02-27 21:14:08 +05:30
lohit
01360d1522 feat/(#1003): oauth2 support - styling fixes, code cleanup (#1674)
* feat/(#1003):  oauth2 support - styling fixes, code cleanup (#1674)
---------
Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-02-27 21:12:34 +05:30
Anoop M D
6729d718cf Merge pull request #526 from j0k3r/feature/prettify-graphql
Add "Prettify GraphQL" button
2024-02-27 02:03:28 +05:30
Anoop M D
17abc19770 chore: fix tests 2024-02-27 02:00:19 +05:30
Anoop M D
13cb71eaef chore: fix tests 2024-02-27 01:43:36 +05:30
Anoop M D
b375620875 chore: fix tests 2024-02-27 01:39:34 +05:30
Anoop M D
1cf8a2f3f1 chore(#1667): graceful handling of none type for backward compatibility 2024-02-27 01:29:10 +05:30
Anoop M D
7c416a99ef Merge pull request #1667 from lohxt1/feature/BRU-18--inherit-auth-mode
feat(#BRU-18) : "inherit" auth mode for inheriting auth to requests from collection
2024-02-27 01:12:15 +05:30
Anoop M D
389a383b99 Merge branch 'main' into feature/BRU-18--inherit-auth-mode 2024-02-27 01:11:55 +05:30
Jeremy Benoist
d1a8f59c79 Add "Prettify GraphQL" button
The goal is to achieve the same behavior from Insomnia which allow to format a GraphQL query using prettier (see 264177b56f/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx (L260-L266)).

I moved the `prettier` deps from `devDependencies` to `dependencies` because it's now used within the application.
I was forced to import `prettier/standalone` & `prettier/parser-graphql` (as it is explained here: https://prettier.io/docs/en/browser.html) otherwise I got that error:

> Couldn't resolve parser "graphql". Parsers must be explicitly added to the standalone bundle.
2024-02-26 17:02:04 +01:00
lohxt1
d477cfc7e1 feat(#BRU-18): reverted local environment file change 2024-02-26 19:34:28 +05:30
lohxt1
e66e26d115 feat(#BRU-18): updated inherit option order in the auth mode select dropdown 2024-02-26 19:31:18 +05:30
lohxt1
7a635810b1 feat(#1655): updated bruno-tests collection with an "inherit auht" request example 2024-02-26 19:11:11 +05:30
lohit
dde6695a43 feat(#1575): make response pane in collection runner screen unaffected by scroll (#1661)
* feat(#1575): make response pane unaffected by scroll
* feat(#1575): styling consistency
2024-02-26 16:46:53 +05:30
lohit
9f81e6dc73 feat(#1003): oauth2 support - resourceOwnerPasswordCredentials, authorization code, client credentials (#1654)
* feat(#1003): oauth2 support
Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-02-26 16:44:38 +05:30
lohxt1
3c2cbe63c4 feat(#1655): inherit auth mode for requests 2024-02-26 14:27:59 +05:30
Anoop M D
a4b13d5c2a fix: fixed awsv4 env var interpolation issue 2024-02-24 00:47:28 +05:30
Anoop M D
064281d438 Merge branch 'main' of github.com:usebruno/bruno 2024-02-23 23:42:45 +05:30
Anoop M D
43c873422f feat: bruno cli awsv4 auth support 2024-02-23 23:42:28 +05:30
Anoop M D
c7c762185e Merge pull request #1600 from mato-meciar/bugfix/empty-pwd-basic-auth
fix(#1545): empty strings encryption
2024-02-21 20:17:12 +05:30
Anoop M D
09c496e516 Merge pull request #1618 from trusta/feat/set-up-ajv-formats-in-scripts
feat(#947): set up ajv-formats in script and test runtimes
2024-02-21 20:08:57 +05:30
lohit
117726a01f feat(#BRU-11) - tailwindcss v3 upgrade (#1597)
* feat(#BRU-11) - tailwindcss v3 upgrade
* feat(#BRU-11) - lock file update to fix PR checks
2024-02-19 17:36:54 +05:30
lohit
e2d754702a feat(#BRU-10) - codeeditor syntax colors for system theme (#1595) 2024-02-19 17:30:49 +05:30
lohit
fee3416c85 feat(#1575) - auto scroll runner output body during collection run (#1588) 2024-02-19 17:29:24 +05:30
Florent Boisselier
a756c49285 feat(#947): set up ajv-formats in script and test runtimes 2024-02-18 14:22:46 +01:00
Martin Meciar
b6abc665a5 fix(#1545): empty strings encryption
enable empty strings to be encrypted
2024-02-15 19:01:07 +01:00
lohit
bd002ca316 feat(#BRU-7) - scrollbar styling for linux and windows (#1589) 2024-02-15 15:00:18 +05:30
Anoop M D
5fece08f4b chore: display bru cli version while running gh workflow 2024-02-14 05:09:25 +05:30
Anoop M D
36b7fbe584 feat: added gh workflow for testing bru cli from npm 2024-02-14 04:58:54 +05:30
Anoop M D
8cb6060558 chore: version bumped to v1.9.0 2024-02-14 04:18:53 +05:30
Anoop M D
50228d2f50 feat(#1009): improve messaging of close collection modal 2024-02-14 03:57:33 +05:30
Eugen Soliar
ea5993fa76 renamed Remove & altered popover description (#1538)
* renamed Remove button to Disconnect & altered popover dialog description accordingly
* altered toast text on success & on error
* fixed namings & replaced anyways with file path
2024-02-14 03:42:43 +05:30
Mateo Gallardo
485b2f48bc Fixed relative paths for file params in Windows (#1564) 2024-02-14 03:38:57 +05:30
João Victor Davim
f8eac3469f feat(#1579): Add file uploading support for CLI (#1572)
* feat(cli): add support for file upload
* fix: remove wrong console log
2024-02-14 03:37:11 +05:30
Anoop M D
2877a88a8a Merge pull request #1577 from sanjai0py/feature/auto-scroll-on-collection-run
feat(#1575) - Auto scroll on collection run update
2024-02-14 03:25:31 +05:30
Anoop M D
b9d50fbba0 Merge branch 'main' into feature/auto-scroll-on-collection-run 2024-02-14 03:25:06 +05:30
Ricardo Silverio
942a895ae0 [Feature] Stop button for runner execution (#1580)
* First attempts
* Sending cancel token in run-folder-event
* Remove logs, update lock
* cancelTokenUid with default value
* Indentation
* Generating token in the main process side
* Removing uuid import
2024-02-14 03:16:41 +05:30
Anoop M D
eab50f01d7 fix(#1521): fixed issue related to recent menu being disabled 2024-02-14 03:06:27 +05:30
James Hall
8287126deb Recent documents menu (#1582)
* Adds recent documents menu.
* Removes erroneous import.
* Open collection from recent document menu.
2024-02-14 02:47:32 +05:30
Anoop M D
d05a86252b feat(#1447): wip on hotkey for save environment 2024-02-13 17:58:10 +05:30
Sanjai Kumar
1f4171d22a Merge branch 'usebruno:main' into feature/auto-scroll-on-collection-run 2024-02-13 16:54:08 +05:30
sanjai0py
7c314d0fed feat(#1575) - auto scroll on collection run update 2024-02-13 16:43:34 +05:30
Ricardo Silverio
3c87c1df69 Fix crash when closing modal of confirmation before exit (#1574) 2024-02-13 15:47:58 +05:30
Mateo Gallardo
64487ad923 Fixed file uploads performance issues (#1562) 2024-02-12 23:48:01 +05:30
Igor Gulyayev
808af3c19a fix(1548): correct import of escaped curl string (#1549) 2024-02-09 01:38:02 +05:30
Anoop M D
7cf5f0d612 fix: fixed junit tests issue on prs 2024-02-09 01:37:19 +05:30
Anoop M D
a69f7ab2a8 fix: fixed junit tests issue on prs 2024-02-09 01:34:16 +05:30
Anoop M D
966718ca66 fix: fixed junit tests issue on prs 2024-02-09 01:28:54 +05:30
Anoop M D
659e22cabf fix: fixed junit tests issue on prs 2024-02-09 01:24:19 +05:30
Anoop M D
aedcaac2bb fix: fixed junit tests issue on prs 2024-02-09 01:21:06 +05:30
Anoop M D
1900bddd37 chore: updated cli version 2024-02-06 05:00:25 +05:30
Anoop M D
33d5d78e85 chore: bumped version to v1.8.0 2024-02-06 03:37:05 +05:30
Anoop M D
12b9a02f7e chore: updated deps 2024-02-06 03:31:45 +05:30
Anoop M D
7c6a043188 chore: fixed bru-lang tests 2024-02-06 03:04:59 +05:30
Anoop M D
a904672555 fix(#1339): fix issue related where query args with % was dissappearing 2024-02-06 00:36:30 +05:30
Anoop M D
123bf198a3 feat(#1496): handling edge cases for ignore config 2024-02-05 03:27:20 +05:30
fredjeck
cb95b5f36a Introduces a new bruno.json configuration property named 'ignore' allowing to specify which files or folders should be ignored in the collection. (#1514)
Also makes sure the behavior of legacy collections is not altered.

Fixes issue #1496
2024-02-05 03:04:54 +05:30
Anoop M D
09e7ea0d4d feat(#1130): file upload schema updates 2024-02-05 02:52:03 +05:30
Max Destors
634f9ca4a2 feat: Multipart Form Data file uploads (#1130)
* Add multipart form files upload support
* clean up
* Fixed electron files browser for Multipart Form files
* Using relative paths for files inside the collection's folder
---------
Co-authored-by: Mateo Gallardo <mateogallardo@gmail.com>
2024-02-04 23:04:18 +05:30
Anoop M D
a97adbb97e fix: fixed theming issues 2024-02-04 15:17:56 +05:30
Anoop M D
4d8c377143 test: added local module scripting example 2024-02-01 16:49:48 +05:30
Rinku Chaudhari
7b6c72c63b fix: getContentType function and save file logic update (#1499) 2024-02-01 13:29:06 +05:30
Anoop M D
72fde80577 Update release-snap.yml 2024-01-31 00:11:08 +05:30
Anoop M D
c666adc0ba fix: fixed github tests workflow issue 2024-01-30 22:26:47 +05:30
Anoop M D
ea7d141d10 chore: release v1.7.1 2024-01-30 22:11:53 +05:30
Graham White
73c0d058c5 fix: incorrectly named env file prevents successful builds (#1485)
Resolves: #1484

Signed-off-by: Graham White <graham_alton@hotmail.com>
2024-01-30 22:06:26 +05:30
Anoop M D
c39b8ff282 fix(#1487): updated deps and bumped versions 2024-01-30 22:04:39 +05:30
Anoop M D
e258e7f5ab fix(#1487): rolling back to vm2 from @n8n/vm2 2024-01-30 22:00:58 +05:30
Anoop M D
c48cb56709 chore: updated deps 2024-01-30 15:20:51 +05:30
Anoop M D
59b9208d89 chore: bumped cli version to v1.4.0 2024-01-30 15:09:15 +05:30
Anoop M D
cfbac39ba8 fix: fixed snap release workflow 2024-01-30 01:55:09 +05:30
212 changed files with 18143 additions and 7595 deletions

View File

@@ -1,12 +0,0 @@
name: Bump Homebrew Cask
on:
release:
types: [published]
jobs:
bump:
runs-on: macos-10.15
steps:
- name: Bump Homebrew Cask
run: brew bump-cask-pr bruno --version "${GITHUB_REF_NAME#v}"

48
.github/workflows/npm-bru-cli.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Bru CLI Tests (npm)
on:
workflow_dispatch:
inputs:
build:
description: 'Test Bru CLI (npm)'
required: true
default: 'true'
# Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168
permissions:
statuses: write
checks: write
contents: write
pull-requests: write
actions: write
jobs:
test:
name: CLI Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Install Bru CLI from NPM
run: npm install -g @usebruno/cli
- name: Display Bru CLI Version
run: bru --version
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Test Report
path: packages/bruno-tests/collection/junit.xml
reporter: java-junit

View File

@@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@@ -21,13 +21,14 @@ jobs:
node-version: 18
- name: Check package-lock.json
run: npm ci
run: npm ci --legacy-peer-deps
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Build Electron app
run: |
npm run build:bruno-common
npm run build:bruno-query
npm run build:graphql-docs
npm run build:web

View File

@@ -4,6 +4,16 @@ on:
branches: [main]
pull_request:
branches: [main]
# Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168
permissions:
statuses: write
checks: write
contents: write
pull-requests: write
actions: write
jobs:
unit-test:
name: Unit Tests
@@ -61,6 +71,7 @@ jobs:
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
- name: Publish Test Report

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
bun.lockb
node_modules
yarn.lock
pnpm-lock.yaml

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,9 +1,9 @@
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
## Let's make bruno better, together !!
## Let's make Bruno better, together !!
We are happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer.
### Technology Stack
@@ -28,10 +28,6 @@ You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) an
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
### Dependencies
- NodeJS v18
### Local Development
```bash
@@ -77,7 +73,7 @@ npm test --workspace=packages/bruno-schema
npm test --workspace=packages/bruno-lang
```
### Raising Pull Request
### Raising Pull Requests
- Please keep the PR's small and focused on one thing
- Please follow the format of creating branches

View File

@@ -1,16 +1,16 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
<img src="/assets/images/logo-transparent.png" width="80"/>
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | **Deutsch** | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md)
[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | **Deutsch** | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
@@ -20,8 +20,55 @@ Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um gemei
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Synchronisation zu erweitern. Wir schätzen den Schutz deiner Daten und glauben, dass sie auf deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
[Download Bruno](https://www.usebruno.com/downloads)
📢 Sehen Sie sich unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Sie können die [Golden Edition](https://www.usebruno.com/pricing) vorbestellen ~~$19~~ **$9** ! <br/>
### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
Sie können Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
```sh
# Auf Mac via Homebrew
brew install bruno
# Auf Windows via Chocolatey
choco install bruno
# Auf Windows via Scoop
scoop bucket add extras
scoop install bruno
# Auf Windows via winget
winget install Bruno.Bruno
# Auf Linux via Snap
snap install bruno
# Auf Linux via Flatpak
flatpak install com.usebruno.Bruno
# Auf Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### Einsatz auf verschiedensten Plattformen 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
@@ -32,6 +79,16 @@ Oder einer Versionskontrolle deiner Wahl
![bruno](/assets/images/version-control.png) <br /><br />
### Sponsoren
#### Gold Sponsoren
<img src="/assets/images/sponsors/samagata.png" width="150"/>
#### Silber Sponsoren
<img src="/assets/images/sponsors/commit-company.png" width="70"/>
### Wichtige Links 📌
- [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269)

View File

@@ -12,16 +12,60 @@
[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | **Español** | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md)
Bruno un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.
Puedes usar git o cualquier otro sistema de control de versiones que prefieras para colaborar en tus colecciones.
Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincronización en la nube a Bruno, en ningún momento. Valoramos tu privacidad y creemos que tus datos deben permanecer en tu dispositivo. Puedes leer nuestra visión a largo plazo [aquí](https://github.com/usebruno/bruno/discussions/269)
Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincronización en la nube a Bruno, en ningún momento. Valoramos tu privacidad y creemos que tus datos deben permanecer en tu dispositivo. Puedes leer nuestra visión a largo plazo [aquí](https://github.com/usebruno/bruno/discussions/269).
[Descarga Bruno](https://www.usebruno.com/downloads).
📢 Mira nuestra charla en la conferencia India FOSS 3.0 [aquí](https://www.youtube.com/watch?v=7bSMFpbcPiY).
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
La mayoría de nuestras funcionalidades son gratis y de código abierto.
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
### Instalación
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.
También puedes instalar Bruno mediante package managers como Homebrew, Chocolatey, Scoop, Flatpak y Apt.
```sh
# En Mac con Homebrew
brew install bruno
# En Windows con Chocolatey
choco install bruno
# En Windows con Scoop
scoop bucket add extras
scoop install bruno
# En Linux con Snap
snap install bruno
# En Linux con Flatpak
flatpak install com.usebruno.Bruno
# En Linux con Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### Ejecútalo en múltiples plataformas 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />

View File

@@ -4,7 +4,7 @@
### Bruno - IDE de código aberto para explorar e testar APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
@@ -22,6 +22,13 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
![bruno](../../assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
A grande maioria dos nossos recursos são gratuitos e de código aberto.
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9** ! <br/>
### Instalação
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
@@ -29,16 +36,26 @@ Bruno está disponível para download como binário [em nosso site](https://www.
Você também pode instalar o Bruno via gerenciadores de pacotes como Homebrew, Chocolatey, Snap e Apt.
```sh
# Mac via Homebrew
# No Mac via Homebrew
brew install bruno
# Windows via Chocolatey
# No Windows via Chocolatey
choco install bruno
# Linux via Snap
# No Windows via Scoop
scoop bucket add extras
scoop install bruno
# No Windows via winget
winget install Bruno.Bruno
# No Linux via Snap
snap install bruno
# Linux via Apt
# No Linux via Flatpak
flatpak install com.usebruno.Bruno
# No Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
@@ -58,14 +75,26 @@ Ou qualquer sistema de controle de versão de sua escolha.
![bruno](../../assets/images/version-control.png) <br /><br />
### Apoiadores
#### Apoiadores Gold
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
#### Apoiadores Silver
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
### Links Importantes 📌
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentação](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [Website](https://www.usebruno.com)
- [Preços](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
- [Github Sponsors](https://github.com/sponsors/helloanoop)
### Showcase 🎥
@@ -75,7 +104,7 @@ Ou qualquer sistema de controle de versão de sua escolha.
### Apoie ❤️
Au-au! Se você gosta do projeto, clique no botão ⭐!!
Au-au! Se você gosta do projeto e deseja apoiar nosso trabalho, considere nos ajudando via [Github Sponsors](https://github.com/sponsors/helloanoop).
### Compartilhe sua experiência 📣
@@ -85,20 +114,6 @@ Se o Bruno ajudou no seu trabalho e/ou no trabalho de sua equipe, por favor, nã
Por favor, verifique [aqui](../publishing/publishing_pt_br.md) mais informações.
### Colabore 👩‍💻🧑‍💻
Fico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md).
Mesmo que você não possa contribuir codificando, não deixe de relatar problemas e solicitar recursos que precisam ser implementados para atender ao contexto de seu dia a dia.
### Authors
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Mantenha Contato 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
@@ -116,6 +131,20 @@ Mesmo que você não possa contribuir codificando, não deixe de relatar problem
A logo é original do [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licença: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
### Colabore 👩‍💻🧑‍💻
Fico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md).
Mesmo que você não possa contribuir codificando, não deixe de relatar problemas e solicitar recursos que precisam ser implementados para atender ao contexto de seu dia a dia.
### Contribuidores
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Licença 📄
[MIT](license.md)

18230
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,5 +49,8 @@
},
"overrides": {
"rollup": "3.2.5"
},
"dependencies": {
"json-bigint": "^1.0.0"
}
}

View File

@@ -1,5 +0,0 @@
ENV=production
NEXT_PUBLIC_ENV=prod
NEXT_PUBLIC_BRUNO_SERVER_API=https://ada.grafnode.com/api

View File

@@ -0,0 +1,3 @@
ENV=production
NEXT_PUBLIC_ENV=prod

View File

@@ -17,10 +17,11 @@
"@fortawesome/react-fontawesome": "^0.1.16",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tailwindcss/forms": "^0.5.7",
"@tippyjs/react": "^4.2.6",
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.6.0",
"@usebruno/schema": "0.7.0",
"axios": "^1.5.1",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
@@ -53,6 +54,7 @@
"pdfjs-dist": "^3.11.174",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"prettier": "^2.7.1",
"qs": "^6.11.0",
"query-string": "^7.0.1",
"react": "18.2.0",
@@ -70,7 +72,6 @@
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",
"tailwindcss": "^2.2.19",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1",
@@ -82,6 +83,7 @@
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"autoprefixer": "^10.4.17",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
@@ -89,8 +91,9 @@
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.7.1",
"postcss": "^8.4.35",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -43,6 +43,7 @@ if (!SERVER_RENDERED) {
'req.getUrl()',
'req.setUrl(url)',
'req.getMethod()',
'req.getAuthMode()',
'req.setMethod(method)',
'req.getHeader(name)',
'req.getHeaders()',

View File

@@ -70,6 +70,15 @@ const AuthMode = ({ collection }) => {
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
Oauth2
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -11,7 +11,7 @@ const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = get(collection, 'root.request.auth.bearer.token');
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,117 @@
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, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
pkce,
[key]: value
}
})
);
};
const handlePKCEToggle = (e) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
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 } = 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}
/>
</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;

View File

@@ -0,0 +1,28 @@
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'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,69 @@
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 } = 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}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2ClientCredentials;

View File

@@ -0,0 +1,20 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,54 @@
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;

View File

@@ -0,0 +1,98 @@
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(() => {
// initalize 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;

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,69 @@
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, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'password',
accessTokenUrl,
username,
password,
scope,
[key]: value
}
})
);
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = 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}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@@ -0,0 +1,20 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'username',
label: 'Username'
},
{
key: 'password',
label: 'Password'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,37 @@
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';
const grantTypeComponentMap = (grantType, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials collection={collection} />;
break;
default:
return <div>TBD</div>;
break;
}
};
const OAuth2 = ({ collection }) => {
const oAuth = get(collection, 'root.request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, collection)}
</StyledWrapper>
);
};
export default OAuth2;

View File

@@ -8,6 +8,7 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode');
@@ -29,6 +30,9 @@ const Auth = ({ collection }) => {
case 'digest': {
return <DigestAuth collection={collection} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}
}
};
@@ -38,7 +42,6 @@ const Auth = ({ collection }) => {
<AuthMode collection={collection} />
</div>
{getAuthView()}
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save

View File

@@ -11,7 +11,7 @@ import StyledWrapper from './StyledWrapper';
const Docs = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);
@@ -40,7 +40,7 @@ const Docs = ({ collection }) => {
{isEditing ? (
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}

View File

@@ -33,6 +33,10 @@ const Info = ({ collection }) => {
<td className="py-2 px-2 text-right">Location&nbsp;:</td>
<td className="py-2 px-2 break-all">{collection.pathname}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Ignored files&nbsp;:</td>
<td className="py-2 px-2 break-all">{collection.brunoConfig.ignore.map((x) => `'${x}'`).join(', ')}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Environments&nbsp;:</td>
<td className="py-2 px-2">{collection.environments?.length || 0}</td>

View File

@@ -12,7 +12,7 @@ const Script = ({ collection }) => {
const requestScript = get(collection, 'root.request.script.req', '');
const responseScript = get(collection, 'root.request.script.res', '');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onRequestScriptEdit = (value) => {
@@ -44,7 +44,7 @@ const Script = ({ collection }) => {
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
@@ -56,7 +56,7 @@ const Script = ({ collection }) => {
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}

View File

@@ -11,7 +11,7 @@ const Tests = ({ collection }) => {
const dispatch = useDispatch();
const tests = get(collection, 'root.request.tests', '');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -30,7 +30,7 @@ const Tests = ({ collection }) => {
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onEdit}
mode="javascript"
onSave={handleSave}

View File

@@ -11,7 +11,7 @@ import StyledWrapper from './StyledWrapper';
const Documentation = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
const preferences = useSelector((state) => state.app.preferences);
@@ -45,7 +45,7 @@ const Documentation = ({ item, collection }) => {
{isEditing ? (
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
value={docs || ''}
onEdit={onEdit}

View File

@@ -2,6 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
@@ -24,6 +25,16 @@ const EnvironmentSelector = ({ collection }) => {
);
});
const handleSettingsIconClick = () => {
setOpenSettingsModal(true);
dispatch(updateEnvironmentSettingsModalVisibility(true));
};
const handleModalClose = () => {
setOpenSettingsModal(false);
dispatch(updateEnvironmentSettingsModalVisibility(false));
};
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const onSelect = (environment) => {
@@ -66,7 +77,7 @@ const EnvironmentSelector = ({ collection }) => {
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>
@@ -74,7 +85,7 @@ const EnvironmentSelector = ({ collection }) => {
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)} />}
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={handleModalClose} />}
</StyledWrapper>
);
};

View File

@@ -81,71 +81,72 @@ const EnvironmentVariables = ({ environment, collection }) => {
return (
<StyledWrapper className="w-full mt-6 mb-6">
<table>
<thead>
<tr>
<td>Enabled</td>
<td>Name</td>
<td>Value</td>
<td>Secret</td>
<td></td>
</tr>
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<td className="text-center">
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="mousetrap"
id={`${index}.name`}
name={`${index}.name`}
value={variable.name}
onChange={formik.handleChange}
/>
<ErrorMessage name={`${index}.name`} />
</td>
<td>
<SingleLineEditor
theme={storedTheme}
collection={collection}
name={`${index}.value`}
value={variable.value}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
/>
</td>
<td>
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.secret`}
checked={variable.secret}
onChange={formik.handleChange}
/>
</td>
<td>
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
<div className="h-[50vh] overflow-y-auto w-full">
<table>
<thead>
<tr>
<td>Enabled</td>
<td>Name</td>
<td>Value</td>
<td>Secret</td>
<td></td>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<td className="text-center">
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="mousetrap"
id={`${index}.name`}
name={`${index}.name`}
value={variable.name}
onChange={formik.handleChange}
/>
<ErrorMessage name={`${index}.name`} />
</td>
<td>
<SingleLineEditor
theme={storedTheme}
collection={collection}
name={`${index}.value`}
value={variable.value}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
/>
</td>
<td>
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.secret`}
checked={variable.secret}
onChange={formik.handleChange}
/>
</td>
<td>
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
+ Add Variable

View File

@@ -0,0 +1,74 @@
import React from 'react';
import path from 'path';
import { useDispatch } from 'react-redux';
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection }) => {
value = value || [];
const dispatch = useDispatch();
const filenames = value
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
return v.split(separator).pop();
});
// title is shown when hovering over the button
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
dispatch(browseFiles())
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
filePaths = filePaths.map((filePath) => {
const collectionDir = collection.pathname;
if (filePath.startsWith(collectionDir)) {
return path.relative(slash(collectionDir), slash(filePath));
}
return filePath;
});
onChange(filePaths);
})
.catch((error) => {
console.error(error);
});
};
const clear = () => {
onChange('');
};
const renderButtonText = (filenames) => {
if (filenames.length == 1) {
return filenames[0];
}
return filenames.length + ' files selected';
};
return filenames.length > 0 ? (
<div
className="btn btn-secondary px-1"
style={{ fontWeight: 400, width: '100%', textOverflow: 'ellipsis', overflowX: 'hidden' }}
title={title}
>
<button className="align-middle" onClick={clear}>
<IconX size={18} />
</button>
&nbsp;
{renderButtonText(filenames)}
</div>
) : (
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
Select Files
</button>
);
};
export default FilePickerEditor;

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel }) => (
const ModalHeader = ({ title, handleCancel, customHeader }) => (
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-header-title">{title}</div> : null}
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×
@@ -54,6 +54,7 @@ const ModalFooter = ({
const Modal = ({
size,
title,
customHeader,
confirmText,
cancelText,
handleCancel,
@@ -99,7 +100,7 @@ const Modal = ({
return (
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} />
<ModalContent>{children}</ModalContent>
<ModalFooter
confirmText={confirmText}

View File

@@ -0,0 +1,85 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.notifications-modal {
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.notifications.bg};
}
.notification-count {
display: flex;
color: white;
position: absolute;
top: -0.625rem;
right: -0.5rem;
margin-right: 0.5rem;
justify-content: center;
font-size: 0.625rem;
border-radius: 50%;
background-color: ${(props) => props.theme.colors.text.yellow};
border: solid 2px ${(props) => props.theme.sidebar.bg};
min-width: 1.25rem;
}
button.mark-as-read {
font-weight: 400 !important;
}
ul.notifications {
background-color: ${(props) => props.theme.notifications.list.bg};
border-right: solid 1px ${(props) => props.theme.notifications.list.borderRight};
min-height: 400px;
height: 100%;
max-height: 85vh;
overflow-y: auto;
li {
min-width: 150px;
cursor: pointer;
padding: 0.5rem 0.625rem;
border-left: solid 2px transparent;
color: ${(props) => props.theme.textLink};
border-bottom: solid 1px ${(props) => props.theme.notifications.list.borderBottom};
&:hover {
background-color: ${(props) => props.theme.notifications.list.hoverBg};
}
&.active {
color: ${(props) => props.theme.text} !important;
background-color: ${(props) => props.theme.notifications.list.active.bg} !important;
border-left: solid 2px ${(props) => props.theme.notifications.list.active.border};
&:hover {
background-color: ${(props) => props.theme.notifications.list.active.hoverBg} !important;
}
}
&.read {
color: ${(props) => props.theme.text} !important;
}
.notification-date {
font-size: 0.6875rem;
}
}
}
.notification-title {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.notification-date {
color: ${(props) => props.theme.colors.text.muted};
}
.pagination {
background-color: ${(props) => props.theme.notifications.list.bg};
border-right: solid 1px ${(props) => props.theme.notifications.list.borderRight};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,191 @@
import { IconBell } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyleWrapper';
import Modal from 'components/Modal/index';
import { useEffect } from 'react';
import {
fetchNotifications,
markAllNotificationsAsRead,
markNotificationAsRead
} from 'providers/ReduxStore/slices/notifications';
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
const PAGE_SIZE = 5;
const Notifications = () => {
const dispatch = useDispatch();
const notifications = useSelector((state) => state.notifications.notifications);
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
const totalPages = Math.ceil(notifications.length / PAGE_SIZE);
const unreadNotifications = notifications.filter((notification) => !notification.read);
useEffect(() => {
dispatch(fetchNotifications());
}, []);
useEffect(() => {
reset();
}, [showNotificationsModal]);
useEffect(() => {
if (!selectedNotification && notifications?.length > 0 && showNotificationsModal) {
let firstNotification = notifications[0];
setSelectedNotification(firstNotification);
dispatch(markNotificationAsRead({ notificationId: firstNotification?.id }));
}
}, [notifications, selectedNotification, showNotificationsModal]);
const reset = () => {
setSelectedNotification(null);
setPageNumber(1);
};
const handlePrev = (e) => {
if (pageNumber - 1 < 1) return;
setPageNumber(pageNumber - 1);
};
const handleNext = (e) => {
if (pageNumber + 1 > totalPages) return;
setPageNumber(pageNumber + 1);
};
const handleNotificationItemClick = (notification) => (e) => {
e.preventDefault();
setSelectedNotification(notification);
dispatch(markNotificationAsRead({ notificationId: notification?.id }));
};
const modalCustomHeader = (
<div className="flex flex-row gap-8">
<div>NOTIFICATIONS</div>
{unreadNotifications.length > 0 && (
<>
<div className="normal-case font-normal">
{unreadNotifications.length} <span>unread notifications</span>
</div>
<button
className={`select-none ${1 == 2 ? 'opacity-50' : 'text-link mark-as-read cursor-pointer hover:underline'}`}
onClick={() => dispatch(markAllNotificationsAsRead())}
>
{'Mark all as read'}
</button>
</>
)}
</div>
);
return (
<StyledWrapper>
<div
className="relative"
onClick={() => {
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
>
<IconBell
size={18}
strokeWidth={1.5}
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
/>
{unreadNotifications.length > 0 && (
<div className="notification-count text-xs">{unreadNotifications.length}</div>
)}
</div>
{showNotificationsModal && (
<Modal
size="lg"
title="Notifications"
confirmText={'Close'}
handleConfirm={() => {
setShowNotificationsModal(false);
}}
handleCancel={() => {
setShowNotificationsModal(false);
}}
hideFooter={true}
customHeader={modalCustomHeader}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<div className="notifications-modal">
{notifications?.length > 0 ? (
<div className="grid grid-cols-4 flex flex-row text-sm">
<div className="col-span-1 flex flex-col">
<ul
className="notifications w-full flex flex-col h-[50vh] max-h-[50vh] overflow-y-auto"
style={{ maxHeight: '50vh', height: '46vh' }}
>
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li
key={notification.id}
className={`p-4 flex flex-col justify-center ${
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
onClick={handleNotificationItemClick(notification)}
>
<div className="notification-title w-full">{notification?.title}</div>
<div className="notification-date text-xs py-2">{relativeDate(notification?.date)}</div>
</li>
))}
</ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handlePrev}
>
{'Prev'}
</button>
<div className="flex flex-row items-center justify-center gap-1">
Page
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{pageNumber}
</div>
of
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{totalPages}
</div>
</div>
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNext}
>
{'Next'}
</button>
</div>
</div>
<div className="flex w-full col-span-3 p-4 flex-col">
<div className="w-full text-lg flex flex-wrap h-fit mb-1">{selectedNotification?.title}</div>
<div className="w-full notification-date text-xs mb-4">
{humanizeDate(selectedNotification?.date)}
</div>
<div
className="flex w-full flex-col flex-wrap h-fit"
dangerouslySetInnerHTML={{ __html: selectedNotification?.description }}
></div>
</div>
</div>
) : (
<div className="opacity-50 italic text-xs p-12 flex justify-center">No Notifications</div>
)}
</div>
</Modal>
)}
</StyledWrapper>
);
};
export default Notifications;

View File

@@ -4,7 +4,6 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
@@ -16,6 +15,7 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
font-weight: 600;
}
td {
padding: 6px 10px;

View File

@@ -38,7 +38,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('awsv4');
}}
>
@@ -47,7 +47,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('basic');
}}
>
@@ -56,7 +56,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('bearer');
}}
>
@@ -65,7 +65,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('digest');
}}
>
@@ -74,7 +74,25 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('oauth2');
}}
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('inherit');
}}
>
Inherit
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('none');
}}
>

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -12,8 +12,8 @@ const BearerAuth = ({ item, collection }) => {
const { storedTheme } = useTheme();
const bearerToken = item.draft
? get(item, 'draft.request.auth.bearer.token')
: get(item, 'request.auth.bearer.token');
? get(item, 'draft.request.auth.bearer.token', '')
: get(item, 'request.auth.bearer.token', '');
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,119 @@
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 { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
pkce,
[key]: value
}
})
);
};
const handlePKCEToggle = (e) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
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 } = 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}
/>
</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;

View File

@@ -0,0 +1,28 @@
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'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,70 @@
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 { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
const OAuth2ClientCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.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 } = 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}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2ClientCredentials;

View File

@@ -0,0 +1,20 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,54 @@
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;

View File

@@ -0,0 +1,92 @@
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';
const GrantTypeSelector = ({ item, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const 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(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType
}
})
);
};
useEffect(() => {
// initalize redux state with a default oauth2 grant type
// authorization_code - default option
!oAuth?.grantType &&
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.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;

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,70 @@
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 { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { accessTokenUrl, username, password, scope } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'password',
accessTokenUrl,
username,
password,
scope,
[key]: value
}
})
);
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = 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}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@@ -0,0 +1,20 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'username',
label: 'Username'
},
{
key: 'password',
label: 'Password'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,37 @@
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';
const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials item={item} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode item={item} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials item={item} collection={collection} />;
break;
default:
return <div>TBD</div>;
break;
}
};
const OAuth2 = ({ item, collection }) => {
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
</StyledWrapper>
);
};
export default OAuth2;

View File

@@ -1,5 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div``;
const Wrapper = styled.div`
.inherit-mode-text {
color: ${(props) => props.theme.colors.text.yellow};
}
.inherit-mode-label {
}
`;
export default Wrapper;

View File

@@ -6,10 +6,15 @@ import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import OAuth2 from './OAuth2/index';
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const collectionRoot = get(collection, 'root', {});
const collectionAuth = get(collectionRoot, 'request.auth');
const getAuthView = () => {
switch (authMode) {
case 'awsv4': {
@@ -24,6 +29,29 @@ const Auth = ({ item, collection }) => {
case 'digest': {
return <DigestAuth collection={collection} item={item} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} item={item} />;
}
case 'inherit': {
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">Cannot inherit Oauth2 from collection.</div>
</div>
) : (
<>
<div>Auth inherited from the Collection: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</>
)}
</div>
);
}
}
};
@@ -36,4 +64,5 @@ const Auth = ({ item, collection }) => {
</StyledWrapper>
);
};
export default Auth;

View File

@@ -27,7 +27,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const variables = item.draft
? get(item, 'draft.request.body.graphql.variables')
: get(item, 'request.body.graphql.variables');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const [schema, setSchema] = useState(null);
useEffect(() => {
@@ -61,7 +61,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
return (
<QueryEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}

View File

@@ -10,7 +10,7 @@ import StyledWrapper from './StyledWrapper';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -31,7 +31,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
<CodeEditor
collection={collection}
value={variables || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onEdit}
mode="javascript"

View File

@@ -12,6 +12,7 @@ import {
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -22,7 +23,18 @@ const MultipartFormParams = ({ item, collection }) => {
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid
collectionUid: collection.uid,
type: 'text'
})
);
};
const addFile = () => {
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
type: 'file'
})
);
};
@@ -92,24 +104,42 @@ const MultipartFormParams = ({ item, collection }) => {
/>
</td>
<td>
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
{param.type === 'file' ? (
<FilePickerEditor
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
collection={collection}
/>
) : (
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
)}
</td>
<td>
<div className="flex items-center">
@@ -131,9 +161,16 @@ const MultipartFormParams = ({ item, collection }) => {
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
<div>
<button className="btn-add-param text-link pr-2 pt-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
</div>
<div>
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
+ Add File
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -8,9 +8,13 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import MD from 'markdown-it';
import { format } from 'prettier/standalone';
import prettierPluginGraphql from 'prettier/parser-graphql';
import { getAllVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { IconWand } from '@tabler/icons';
import onHasCompletion from './onHasCompletion';
@@ -178,6 +182,20 @@ export default class QueryEditor extends React.Component {
}
}
beautifyRequestBody = () => {
try {
const prettyQuery = format(this.props.value, {
parser: 'graphql',
plugins: [prettierPluginGraphql]
});
this.editor.setValue(prettyQuery);
toast.success('Query prettified');
} catch (e) {
toast.error('Error occurred while prettifying GraphQL query');
}
};
// Todo: Overlay is messing up with schema hint
// Fix this
addOverlay = () => {
@@ -189,13 +207,23 @@ export default class QueryEditor extends React.Component {
render() {
return (
<StyledWrapper
className="h-full w-full"
aria-label="Query Editor"
ref={(node) => {
this._node = node;
}}
/>
<>
<StyledWrapper
className="h-full w-full relative"
aria-label="Query Editor"
ref={(node) => {
this._node = node;
}}
>
<button
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
onClick={this.beautifyRequestBody}
title="prettify"
>
<IconWand size={20} strokeWidth={1.5} />
</button>
</StyledWrapper>
</>
);
}

View File

@@ -8,6 +8,7 @@ import { humanizeRequestBodyMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
import { toastError } from 'utils/common/error';
import jsonBigint from 'json-bigint';
const RequestBodyMode = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -37,8 +38,8 @@ const RequestBodyMode = ({ item, collection }) => {
const onPrettify = () => {
if (body?.json && bodyMode === 'json') {
try {
const bodyJson = JSON.parse(body.json);
const prettyBodyJson = JSON.stringify(bodyJson, null, 2);
const bodyJson = jsonBigint.parse(body.json);
const prettyBodyJson = jsonBigint.stringify(bodyJson, null, 2);
dispatch(
updateRequestBody({
content: prettyBodyJson,

View File

@@ -13,7 +13,7 @@ const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -48,7 +48,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}

View File

@@ -12,7 +12,7 @@ const Script = ({ item, collection }) => {
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onRequestScriptEdit = (value) => {
@@ -45,7 +45,7 @@ const Script = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onRequestScriptEdit}
mode="javascript"
@@ -58,7 +58,7 @@ const Script = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onResponseScriptEdit}
mode="javascript"

View File

@@ -11,7 +11,7 @@ const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -32,7 +32,7 @@ const Tests = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onEdit}
mode="javascript"

View File

@@ -12,6 +12,7 @@ const ConfirmRequestClose = ({ item, onCancel, onCloseWithoutSave, onSaveAndClos
disableEscapeKey={true}
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
handleCancel={onCancel}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();

View File

@@ -28,6 +28,19 @@ const RequestTab = ({ tab, collection }) => {
);
};
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
}
};
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
@@ -124,7 +137,18 @@ const RequestTab = ({ tab, collection }) => {
}}
/>
)}
<div className="flex items-baseline tab-label pl-2">
<div
className="flex items-baseline tab-label pl-2"
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}
}}
>
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>

View File

@@ -1,8 +1,24 @@
import { IconFilter } from '@tabler/icons';
import { IconFilter, IconX } from '@tabler/icons';
import React, { useMemo } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
const QueryResultFilter = ({ onChange, mode }) => {
const QueryResultFilter = ({ filter, onChange, mode }) => {
const inputRef = useRef(null);
const [isExpanded, toggleExpand] = useState(false);
const handleFilterClick = () => {
// Toggle filter search bar
toggleExpand(!isExpanded);
// Reset filter search input
onChange({ target: { value: '' } });
// Reset input value
if (inputRef?.current) {
inputRef.current.value = '';
}
};
const tooltipText = useMemo(() => {
if (mode.includes('json')) {
return 'Filter with JSONPath';
@@ -28,16 +44,14 @@ const QueryResultFilter = ({ onChange, mode }) => {
}, [mode]);
return (
<div className={'response-filter relative'}>
<div className="absolute inset-y-0 left-0 pl-4 flex items-center">
<div className="text-gray-500 sm:text-sm" id="request-filter-icon">
<IconFilter size={16} strokeWidth={1.5} />
</div>
</div>
{tooltipText && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
<div
className={
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2'
}
>
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
<input
ref={inputRef}
type="text"
name="response-filter"
id="response-filter"
@@ -46,9 +60,14 @@ const QueryResultFilter = ({ onChange, mode }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="block w-full pl-10 py-1 sm:text-sm"
className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100' : 'w-[0] opacity-0'
}`}
onChange={onChange}
/>
<div className="text-gray-500 sm:text-sm cursor-pointer" id="request-filter-icon" onClick={handleFilterClick}>
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}
</div>
</div>
);
};

View File

@@ -19,7 +19,7 @@ const QueryResultPreview = ({
collection,
mode,
disableRunEventListener,
storedTheme
displayedTheme
}) => {
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
@@ -71,7 +71,7 @@ const QueryResultPreview = ({
<CodeEditor
collection={collection}
font={get(preferences, 'font.codeFont', 'default')}
theme={storedTheme}
theme={displayedTheme}
onRun={onRun}
value={formattedData}
mode={mode}

View File

@@ -3,7 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
display: grid;
grid-template-columns: 100%;
grid-template-rows: ${(props) => (props.queryFilterEnabled ? '1.25rem 1fr 2.25rem' : '1.25rem 1fr')};
grid-template-rows: 1.25rem 1fr;
/* This is a hack to force Codemirror to use all available space */
> div {

View File

@@ -51,7 +51,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
const formattedData = formatResponse(data, mode, filter);
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const debouncedResultFilterOnChange = debounce((e) => {
setFilter(e.target.value);
@@ -132,9 +132,11 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
storedTheme={storedTheme}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && <QueryResultFilter onChange={debouncedResultFilterOnChange} mode={mode} />}
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
)}
</>
)}
</StyledWrapper>

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import path from 'path';
import { useDispatch } from 'react-redux';
import { get, cloneDeep } from 'lodash';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
@@ -24,14 +24,26 @@ export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null);
// ref for the runner output body
const runnerBodyRef = useRef();
const autoScrollRunnerBody = () => {
if (runnerBodyRef?.current) {
// mimicks the native terminal scroll style
runnerBodyRef.current.scrollTo(0, 100000);
}
};
useEffect(() => {
if (!collection.runnerResult) {
setSelectedItem(null);
}
autoScrollRunnerBody();
}, [collection, setSelectedItem]);
const collectionCopy = cloneDeep(collection);
const runnerInfo = get(collection, 'runnerResult.info', {});
const items = cloneDeep(get(collection, 'runnerResult.items', []))
.map((item) => {
const info = findItemInCollection(collectionCopy, item.uid);
@@ -81,6 +93,10 @@ export default function RunnerResults({ collection }) {
);
};
const cancelExecution = () => {
dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));
};
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
const passedRequests = items.filter((item) => {
return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
@@ -91,12 +107,11 @@ export default function RunnerResults({ collection }) {
if (!items || !items.length) {
return (
<StyledWrapper className="px-4">
<StyledWrapper className="px-4 pb-4">
<div className="font-medium mt-6 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
<div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div>
@@ -114,13 +129,23 @@ export default function RunnerResults({ collection }) {
return (
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative">
<div className="font-medium mt-6 mb-4 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
<div className="flex flex-row">
<div className="font-medium my-6 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
<button className="btn ml-6 my-4 btn-sm btn-danger" onClick={cancelExecution}>
Cancel Execution
</button>
)}
</div>
<div className="flex flex-1">
<div className="flex flex-col flex-1">
<div className="py-2 font-medium test-summary">
<div className="flex flex-row gap-4">
<div
className="flex flex-col flex-1 overflow-y-auto h-[calc(100vh_-_12rem)] max-h-[calc(100vh_-_12rem)] w-full"
ref={runnerBodyRef}
>
<div className="pb-2 font-medium test-summary">
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
</div>
{items.map((item) => {
@@ -195,7 +220,6 @@ export default function RunnerResults({ collection }) {
</div>
);
})}
{runnerInfo.status === 'ended' ? (
<div className="mt-2 mb-4">
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
@@ -210,8 +234,8 @@ export default function RunnerResults({ collection }) {
</div>
) : null}
</div>
<div className="flex flex-1" style={{ width: '50%' }}>
{selectedItem ? (
{selectedItem ? (
<div className="flex flex-1 w-[50%]">
<div className="flex flex-col w-full overflow-auto">
<div className="flex items-center px-3 mb-4 font-medium">
<span className="mr-2">{selectedItem.relativePath}</span>
@@ -226,8 +250,8 @@ export default function RunnerResults({ collection }) {
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
<ResponsePane item={selectedItem} collection={collection} />
</div>
) : null}
</div>
</div>
) : null}
</div>
</StyledWrapper>
);

View File

@@ -11,7 +11,7 @@ import { IconCopy } from '@tabler/icons';
import { findCollectionByItemUid } from '../../../../../../../utils/collections/index';
const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const { target, client, language: lang } = language;
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
@@ -45,7 +45,7 @@ const CodeView = ({ language, item }) => {
readOnly
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
theme={storedTheme}
theme={displayedTheme}
mode={lang}
/>
</StyledWrapper>

View File

@@ -2,6 +2,7 @@ import React from 'react';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { IconFiles } from '@tabler/icons';
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
const RemoveCollection = ({ onClose, collection }) => {
@@ -10,15 +11,25 @@ const RemoveCollection = ({ onClose, collection }) => {
const onConfirm = () => {
dispatch(removeCollection(collection.uid))
.then(() => {
toast.success('Collection removed');
toast.success('Collection closed');
onClose();
})
.catch(() => toast.error('An error occurred while removing the collection'));
.catch(() => toast.error('An error occurred while closing the collection'));
};
return (
<Modal size="sm" title="Remove Collection" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
Are you sure you want to remove collection <span className="font-semibold">{collection.name}</span> ?
<Modal size="sm" title="Close Collection" confirmText="Close" handleConfirm={onConfirm} handleCancel={onClose}>
<div className="flex items-center">
<IconFiles size={18} strokeWidth={1.5} />
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="break-words text-xs mt-1">{collection.pathname}</div>
<div className="mt-4">
Are you sure you want to close collection <span className="font-semibold">{collection.name}</span> in Bruno?
</div>
<div className="mt-4">
It will still be available in the file system at the above location and can be re-opened later.
</div>
</Modal>
);
};

View File

@@ -217,7 +217,7 @@ const Collection = ({ collection, searchText }) => {
setShowRemoveCollectionModal(true);
}}
>
Remove
Close
</div>
<div
className="dropdown-item"

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import Modal from 'components/Modal/index';
import { PostHog } from 'posthog-node';
import { uuid } from 'utils/common';
import { IconHeart, IconUser, IconUsers } from '@tabler/icons';
import { IconHeart, IconUser, IconUsers, IconPlus } from '@tabler/icons';
import platformLib from 'platform';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme/index';
@@ -59,7 +59,7 @@ const CheckIcon = () => {
};
const GoldenEdition = ({ onClose }) => {
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
useEffect(() => {
const anonymousId = getAnonymousTrackingId();
@@ -85,11 +85,10 @@ const GoldenEdition = ({ onClose }) => {
});
};
const goldenEditon = [
const goldenEditonIndividuals = [
'Inbuilt Bru File Explorer',
'Visual Git (Like Gitlens for Vscode)',
'GRPC, Websocket, SocketIO, MQTT',
'Intergration with Secret Managers',
'Load Data from File for Collection Run',
'Developer Tools',
'OpenAPI Designer',
@@ -98,16 +97,24 @@ const GoldenEdition = ({ onClose }) => {
'Custom Themes'
];
const goldenEditonOrganizations = [
'Centralized License Management',
'Intergration with Secret Managers',
'Private Collection Registry',
'Request Forms',
'Priority Support'
];
const [pricingOption, setPricingOption] = useState('individuals');
const handlePricingOptionChange = (option) => {
setPricingOption(option);
};
const themeBasedContainerClassNames = storedTheme === 'light' ? 'text-gray-900' : 'text-white';
const themeBasedTabContainerClassNames = storedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800';
const themeBasedContainerClassNames = displayedTheme === 'light' ? 'text-gray-900' : 'text-white';
const themeBasedTabContainerClassNames = displayedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800';
const themeBasedActiveTabClassNames =
storedTheme === 'light' ? 'bg-white text-gray-900 font-medium' : 'bg-gray-700 text-white font-medium';
displayedTheme === 'light' ? 'bg-white text-gray-900 font-medium' : 'bg-gray-700 text-white font-medium';
return (
<StyledWrapper>
@@ -123,8 +130,7 @@ const GoldenEdition = ({ onClose }) => {
target="_blank"
className="flex text-white bg-yellow-600 hover:bg-yellow-700 font-medium rounded-lg text-sm px-4 py-2 text-center cursor-pointer"
>
<IconHeart size={18} strokeWidth={1.5} />{' '}
<span className="ml-2">{pricingOption === 'individuals' ? 'Buy' : 'Subscribe'}</span>
<IconHeart size={18} strokeWidth={1.5} /> <span className="ml-2">Buy</span>
</a>
</div>
{pricingOption === 'individuals' ? (
@@ -138,9 +144,11 @@ const GoldenEdition = ({ onClose }) => {
) : (
<div>
<div className="my-4">
<span className="text-3xl font-extrabold">$5</span>
<span className="text-3xl font-extrabold">$49</span>
<span className="ml-2">/&nbsp;user</span>
</div>
<p>/user/month</p>
<p className="bg-yellow-200 text-black rounded-md px-2 py-1 mb-2 inline-flex text-sm">One Time Payment</p>
<p className="text-sm">perpetual license with 2 years of updates</p>
</div>
)}
<div
@@ -169,12 +177,29 @@ const GoldenEdition = ({ onClose }) => {
<HeartIcon />
<span>Support Bruno's Development</span>
</li>
{goldenEditon.map((item, index) => (
<li className="flex items-center space-x-3" key={index}>
<CheckIcon />
<span>{item}</span>
</li>
))}
{pricingOption === 'individuals' ? (
<>
{goldenEditonIndividuals.map((item, index) => (
<li className="flex items-center space-x-3" key={index}>
<CheckIcon />
<span>{item}</span>
</li>
))}
</>
) : (
<>
<li className="flex items-center space-x-3 pb-4">
<IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />
<span>Everything in the Individual Plan</span>
</li>
{goldenEditonOrganizations.map((item, index) => (
<li className="flex items-center space-x-3" key={index}>
<CheckIcon />
<span>{item}</span>
</li>
))}
</>
)}
</ul>
</div>
</Modal>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import importBrunoCollection from 'utils/importers/bruno-collection';
import importPostmanCollection from 'utils/importers/postman-collection';
import importInsomniaCollection from 'utils/importers/insomnia-collection';
@@ -7,6 +7,13 @@ import { toastError } from 'utils/common/error';
import Modal from 'components/Modal';
const ImportCollection = ({ onClose, handleSubmit }) => {
const [options, setOptions] = useState({
enablePostmanTranslations: {
enabled: true,
label: 'Auto translate postman scripts',
subLabel: "When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
}
})
const handleImportBrunoCollection = () => {
importBrunoCollection()
.then((collection) => {
@@ -16,7 +23,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
};
const handleImportPostmanCollection = () => {
importPostmanCollection()
importPostmanCollection(options)
.then((collection) => {
handleSubmit(collection);
})
@@ -38,21 +45,66 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
})
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
};
const toggleOptions = (event, optionKey) => {
setOptions({ ...options, [optionKey]: {
...options[optionKey],
enabled: !options[optionKey].enabled
} });
};
const CollectionButton = ({ children, className, onClick }) => {
return (
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}
</button>
)
}
return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<div>
<div className="text-link hover:underline cursor-pointer" onClick={handleImportBrunoCollection}>
Bruno Collection
<div className="flex flex-col">
<h3 className="text-sm">Select the type of your existing collection :</h3>
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
<CollectionButton onClick={handleImportBrunoCollection}>
Bruno Collection
</CollectionButton>
<CollectionButton onClick={handleImportPostmanCollection}>
Postman Collection
</CollectionButton>
<CollectionButton onClick={handleImportInsomniaCollection}>
Insomnia Collection
</CollectionButton>
<CollectionButton onClick={handleImportOpenapiCollection}>
OpenAPI V3 Spec
</CollectionButton>
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportPostmanCollection}>
Postman Collection
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
Insomnia Collection
</div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
OpenAPI V3 Spec
<div className="flex justify-start w-full mt-4 max-w-[450px]">
{Object.entries(options || {}).map(([key, option]) => (
<div className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id="comments"
aria-describedby="comments-description"
name="comments"
type="checkbox"
checked={option.enabled}
onChange={(e) => toggleOptions(e,key)}
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
/>
</div>
<div className="ml-2 text-sm leading-6">
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
{option.label}
</label>
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
{option.subLabel}
</p>
</div>
</div>
))}
</div>
</div>
</Modal>

View File

@@ -45,7 +45,11 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
const onSubmit = () => formik.handleSubmit();
return (
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
<Modal
size="sm"
title="Import Collection"
confirmText="Import"
handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="collectionName" className="block font-semibold">

View File

@@ -11,6 +11,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { IconSettings, IconCookie, IconHeart } from '@tabler/icons';
import { updateLeftSidebarWidth, updateIsDragging, showPreferences } from 'providers/ReduxStore/slices/app';
import { useTheme } from 'providers/Theme';
import Notifications from 'components/Notifications';
const MIN_LEFT_SIDEBAR_WIDTH = 221;
const MAX_LEFT_SIDEBAR_WIDTH = 600;
@@ -112,6 +113,7 @@ const Sidebar = () => {
className="mr-2 hover:text-gray-700"
onClick={() => setGoldenEditonOpen(true)}
/>
<Notifications />
</div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
{/* This will get moved to home page */}
@@ -124,7 +126,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.7.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.12.2</div>
</div>
</div>
</div>

View File

@@ -49,6 +49,10 @@ const StyledWrapper = styled.div`
padding-left: 0;
padding-right: 0;
}
.CodeMirror-selected {
background-color: rgba(212, 125, 59, 0.3);
}
}
`;

View File

@@ -84,13 +84,13 @@ const Welcome = () => {
<span className="label ml-2">Documentation</span>
</a>
</div>
<div className="mt-2">
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
</a>
</div>
<div className="mt-2">
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span>

View File

@@ -159,6 +159,33 @@ const GlobalStyle = createGlobalStyle`
}
}
// scrollbar styling
// the below media query target non-macos devices
// (macos scrollbar styling is the ideal style reference)
@media not all and (pointer: coarse) {
* {
scrollbar-width: thin;
scrollbar-color: ${(props) => props.theme.scrollbar.color};
}
*::-webkit-scrollbar {
width: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
*::-webkit-scrollbar-thumb {
background-color: ${(props) => props.theme.scrollbar.color};
border-radius: 14px;
border: 3px solid ${(props) => props.theme.scrollbar.color};
}
}
// codemirror
.CodeMirror {
.cm-variable-valid {

View File

@@ -10,7 +10,6 @@ import ErrorBoundary from './ErrorBoundary';
import '../styles/app.scss';
import '../styles/globals.css';
import 'tailwindcss/dist/tailwind.min.css';
import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.7.0'
version: '1.12.2'
}
});
};

View File

@@ -18,6 +18,7 @@ export const HotkeysProvider = (props) => {
const tabs = useSelector((state) => state.tabs.tabs);
const collections = useSelector((state) => state.collections.collections);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
@@ -43,16 +44,20 @@ export const HotkeysProvider = (props) => {
// save hotkey
useEffect(() => {
Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if (collection) {
const item = findItemInCollection(collection, activeTab.uid);
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
} else {
// todo: when ephermal requests go live
// setShowSaveRequestModal(true);
if (isEnvironmentSettingsModalOpen) {
console.log('todo: save environment settings');
} else {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if (collection) {
const item = findItemInCollection(collection, activeTab.uid);
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
} else {
// todo: when ephermal requests go live
// setShowSaveRequestModal(true);
}
}
}
}
@@ -63,7 +68,7 @@ export const HotkeysProvider = (props) => {
return () => {
Mousetrap.unbind(['command+s', 'ctrl+s']);
};
}, [activeTabUid, tabs, saveRequest, collections]);
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]);
// send request (ctrl/cmd + enter)
useEffect(() => {

View File

@@ -5,6 +5,7 @@ import debugMiddleware from './middlewares/debug/middleware';
import appReducer from './slices/app';
import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications';
const { publicRuntimeConfig } = getConfig();
const isDevEnv = () => {
@@ -20,7 +21,8 @@ export const store = configureStore({
reducer: {
app: appReducer,
collections: collectionsReducer,
tabs: tabsReducer
tabs: tabsReducer,
notifications: notificationsReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
});

View File

@@ -9,6 +9,7 @@ const initialState = {
screenWidth: 500,
showHomePage: false,
showPreferences: false,
isEnvironmentSettingsModalOpen: false,
preferences: {
request: {
sslVerification: true,
@@ -42,6 +43,9 @@ export const appSlice = createSlice({
updateIsDragging: (state, action) => {
state.isDragging = action.payload.isDragging;
},
updateEnvironmentSettingsModalVisibility: (state, action) => {
state.isEnvironmentSettingsModalOpen = action.payload;
},
showHomePage: (state) => {
state.showHomePage = true;
},
@@ -74,6 +78,7 @@ export const {
refreshScreenWidth,
updateLeftSidebarWidth,
updateIsDragging,
updateEnvironmentSettingsModalVisibility,
showHomePage,
hideHomePage,
showPreferences,

View File

@@ -40,6 +40,7 @@ import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -138,6 +139,35 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
});
};
export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
} else {
toast.success('Request made successfully');
}
return response;
})
.then(resolve)
.catch((err) => {
toast.error(err.message);
});
});
};
export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -147,7 +177,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
return reject(new Error('Collection not found'));
}
const itemCopy = cloneDeep(item);
const itemCopy = cloneDeep(item || {});
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
@@ -208,6 +238,10 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
.catch((err) => console.log(err));
};
export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
};
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -909,6 +943,16 @@ export const browseDirectory = () => (dispatch, getState) => {
});
};
export const browseFiles =
(filters = []) =>
(dispatch, getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
});
};
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();

View File

@@ -402,6 +402,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'digest';
item.draft.request.auth.digest = action.payload.content;
break;
case 'oauth2':
item.draft.request.auth.mode = 'oauth2';
item.draft.request.auth.oauth2 = action.payload.content;
break;
}
}
}
@@ -617,6 +621,7 @@ export const collectionsSlice = createSlice({
item.draft.request.body.multipartForm = item.draft.request.body.multipartForm || [];
item.draft.request.body.multipartForm.push({
uid: uuid(),
type: action.payload.type,
name: '',
value: '',
description: '',
@@ -637,6 +642,7 @@ export const collectionsSlice = createSlice({
}
const param = find(item.draft.request.body.multipartForm, (p) => p.uid === action.payload.param.uid);
if (param) {
param.type = action.payload.param.type;
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
@@ -672,6 +678,7 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.auth = {};
item.draft.request.auth.mode = action.payload.mode;
}
}
@@ -972,6 +979,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode);
}
},
@@ -979,6 +987,8 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode);
switch (action.payload.mode) {
case 'awsv4':
set(collection, 'root.request.auth.awsv4', action.payload.content);
@@ -992,6 +1002,9 @@ export const collectionsSlice = createSlice({
case 'digest':
set(collection, 'root.request.auth.digest', action.payload.content);
break;
case 'oauth2':
set(collection, 'root.request.auth.oauth2', action.payload.content);
break;
}
}
},
@@ -1288,7 +1301,7 @@ export const collectionsSlice = createSlice({
}
},
runFolderEvent: (state, action) => {
const { collectionUid, folderUid, itemUid, type, isRecursive, error } = action.payload;
const { collectionUid, folderUid, itemUid, type, isRecursive, error, cancelTokenUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
@@ -1304,6 +1317,7 @@ export const collectionsSlice = createSlice({
info.collectionUid = collectionUid;
info.folderUid = folderUid;
info.isRecursive = isRecursive;
info.cancelTokenUid = cancelTokenUid;
info.status = 'started';
}

Some files were not shown because too many files have changed in this diff Show More