Compare commits

...

140 Commits

Author SHA1 Message Date
Anoop M D
1804454ff0 chore: bump version v0.16.6 2023-10-02 05:26:47 +05:30
Anoop M D
3c710120b9 Merge pull request #274 from lared/bugfix/drag-and-drop-ordering
fix(#154): correct ordering during drag-and-drop
2023-10-02 04:48:43 +05:30
Anoop M D
fcc12fb089 fix(#251, #265): phantoms folders fix on rename/delete needs to be run only on windows 2023-10-02 04:17:35 +05:30
Anoop M D
3d8dee944f Merge pull request #266 from not-known-person/fix-251-265
fixed issue-#251-&-#265,
2023-10-02 04:03:31 +05:30
Anoop M D
78e5cd3c03 Merge pull request #264 from Its-treason/bugfix/insomnia-import-name-collision
fix(#257): Name collision during Insomnia collection import
2023-10-02 03:37:40 +05:30
Its-treason
26d99c7aee fix(#257): Name collision during Insomnia collection import 2023-10-01 23:47:43 +02:00
Anoop M D
77a7318dfb Merge pull request #272 from not-known-person/feat/-#220
added feat/-#220
2023-10-02 02:55:24 +05:30
not-known-person
b83da46f12 added feat/-#220 2023-10-01 21:01:39 -04:00
not-known-person
cf6ec4e84f fixed issue-#251-&-#265 2023-10-01 15:22:05 -04:00
Michał Szymborski
978d810473 fix(#154): correct ordering during drag-and-drop
When dragging and dropping items, to delay changing sequence numbers
until after all the resource-dependent logic has completed, we were
relying on the order of children in redux store (which we then converted
into new seq numbers).

This order of children was however not updated when sequence numbers
changed (for example due to file watch changes). This resulted in a
seemingly random drag-and-drop ordering, which in fact was linked to the
initial order when the collection was loaded.

This change sorts all the items by sequence number prior to reordering,
so that those random jumps no longer happen. As this happens on a deep
clone of the collection, no data gets hurt in the process.

fixes #154
2023-10-01 21:09:11 +02:00
Anoop M D
e83c2da798 chore: version bumped to v0.16.5 2023-10-01 02:17:28 +05:30
Anoop M D
39f148267e Merge pull request #262 from mirkogolze/feature/about-window
#203 #254 #231 add icon to asar content, change styling to allow copying the content
2023-10-01 01:51:32 +05:30
Mirko Golze
f4f093d4db #203 #254 #231
add icon to asar content, change styling to allow copying the content
2023-09-30 21:56:21 +02:00
Anoop M D
3c4ef2f2df Merge pull request #256 from brahma-dev/main
Ensure that adjacent variables are rendered in separate spans.
2023-10-01 00:19:46 +05:30
Anoop M D
e39975cb3c Merge pull request #259 from sadn1ck/fix/gql-docs-build-error
fix(graphql-docs/build): rollup error thrown during build
2023-10-01 00:12:12 +05:30
Anoop M D
b767ccd063 Merge pull request #260 from Its-treason/feature/update-create-collection-form
feat: Update create collection form
2023-09-30 22:09:54 +05:30
Anoop M D
c5adfd8975 Merge pull request #261 from andypiper/bugfix/grammar-typo
Remove rogue apostrophes and capitalise API in text.
2023-09-30 22:05:01 +05:30
Andy Piper
6695d90609 Remove rogue apostrophes and capitalise API in text.
Signed-off-by: Andy Piper <andypiper@users.noreply.github.com>
2023-09-30 15:37:45 +01:00
Its-treason
5c79282a1b feat: Update create collection form
- Remove Name tooltip
- Update Folder Name tooltip
- Move Folder Name input under location
- Update Folder Name validation
  - Now only allow characters for valid system folder names
- Update label htmlFor ids to input ids
2023-09-30 14:35:37 +02:00
Anik Das
64019f8ecf fix(graphql-docs/build): rollup error thrown during build
- during the dts transformation, the css import was not recognized, hence marking it as external in the dts transform didn't throw the error

- "extract: true" in postcss plugin makes sure it gets extracted to the final bundle as well

Signed-off-by: Anik Das <anikdas0811@gmail.com>
2023-09-30 09:49:02 +05:30
Brahma Dev
4c89f31934 Decrease likelihood of any unintentional classname clash. 2023-09-29 21:58:46 +00:00
Brahma Dev
1c53ce91f0 Ignore the randomly generated classname. 2023-09-29 21:42:15 +00:00
Brahma Dev
0b83fbb7ce Add a randomly generated classname to each variable so that CodeMirror does not merge adjacent variables into the same SPAN. 2023-09-29 21:41:54 +00:00
Anoop M D
08ceed86a8 chore: bump version to v0.16.4 2023-09-30 02:48:20 +05:30
Anoop M D
f99918d725 fix(#229): fixed handling of non-JSON/XML content types 2023-09-30 02:44:30 +05:30
Anoop M D
1877dd858e chore: remove unused var 2023-09-30 02:42:10 +05:30
Anoop M D
acff0c379e fix(#236): insomnia import fix 2023-09-30 02:24:31 +05:30
Anoop M D
a849e4fb7b Merge pull request #255 from Its-treason/bugfix/create-collection-modal-validation
Bugfix: Create collection modal validation
2023-09-30 01:53:32 +05:30
Anoop M D
613699fb69 fix(#248): gracefully abort cm header hint when word match fails 2023-09-30 01:44:54 +05:30
Anoop M D
0c4ba71922 feat(#229): added error boundary to capture error trace and allow users to continue using the app 2023-09-30 01:32:05 +05:30
Its-treason
d346bb00eb Merge branch 'main' into bugfix/create-collection-modal-validation 2023-09-29 21:45:01 +02:00
Its-treason
8bb57aa41d fix: location validation in create collection form 2023-09-29 21:44:37 +02:00
Anoop M D
f378f04fc3 Merge pull request #248 from brahma-dev/main
Autocomplete
2023-09-30 00:35:22 +05:30
Brahma Dev
a02d2b9c58 Add standard http headers for autocomplete 2023-09-29 11:59:09 +00:00
Brahma Dev
21edfbc25a Use Codemirror Hint feature for autocomplete 2023-09-29 11:59:01 +00:00
Anoop M D
45042cd52a Merge pull request #243 from LesageYann/feat/allow-async-tests
feat: allow test to be asynchrone
2023-09-29 12:42:57 +05:30
Lesage Yann
314e8c17d3 feat: allow test to be asynchrone 2023-09-29 08:55:26 +02:00
Anoop M D
69a7c0e4ce chore: bumped release version to v0.16.3 2023-09-29 12:24:17 +05:30
Anoop M D
626d925ad6 Merge pull request #242 from tpyle/bug/handle-collection-names-with-slashes
Corrects issue when collection names in imports have slashes
2023-09-29 12:20:11 +05:30
Thomas Pyle
2c0ccf769c Corrects issue when collection names in imports have slashes 2023-09-28 21:42:46 -04:00
Anoop M D
516411b9a2 Merge pull request #240 from gkohen/main
Make sure path string does not overflow the dialog
2023-09-29 02:29:01 +05:30
Gabriel Kohen
60e3f3bb6a Make sure path string does not overflow the dialog
See also: #217
2023-09-28 16:22:40 -04:00
Anoop M D
a6b19605b5 Merge pull request #238 from jsoref/spelling
Spelling
2023-09-29 00:25:43 +05:30
Josh Soref
7ba471f26a spelling: serialization
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
f23dcf50a4 spelling: separator
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
86cda2cf5a spelling: sample
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
00b6e007af spelling: people
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
7313d1b4d7 spelling: occurred
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
8f803234ce spelling: javascript
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
76a743b74e spelling: interpreted
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
c623aa0909 spelling: header
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
3eb26834c7 spelling: github
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
64a5852227 spelling: evaluated
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
6471ca74c3 spelling: ephemeral
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
f77d955839 spelling: environments
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Josh Soref
9947a55b8d spelling: bottom
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Josh Soref
a71555725c spelling: being
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Anoop M D
c9ec6902a5 Merge pull request #234 from brahma-dev/main
Allow tabs in tablists to wrap.
2023-09-28 18:58:10 +05:30
Brahma Dev
c9c675e187 Allow tabs in tablists to wrap. 2023-09-28 13:11:21 +00:00
Anoop M D
0517b2685e fix(#233): bru cli fix for content header parsing issue 2023-09-28 18:31:42 +05:30
Anoop M D
5d01c0a765 chore: bump version to 0.16.2 2023-09-28 18:25:15 +05:30
Anoop M D
f3925923c9 fix(#233): fixed content type env var parsing issue 2023-09-28 18:24:10 +05:30
Anoop M D
6facdfd66b chore: bump version to v0.16.1 2023-09-28 10:27:45 +05:30
Anoop M D
0f211131b1 feat(#224): proxy config support in collection runner 2023-09-28 10:20:31 +05:30
Anoop M D
cd3b8a948e fix(#227): fixed json formatting issue 2023-09-28 10:15:54 +05:30
Anoop M D
f695036721 feat(#224): Bru CLI support for proxying requests 2023-09-28 05:26:09 +05:30
Anoop M D
3661fa7df3 chore: published libs 2023-09-28 05:17:59 +05:30
Anoop M D
559fcb0806 Merge pull request #225 from mirkogolze/feature/191-interpolate-header-names
#191 interpolate header names with variables
2023-09-28 04:38:37 +05:30
Anoop M D
d5da8a9e2f chore: bump version to v0.16.0 2023-09-28 04:34:34 +05:30
Anoop M D
a3050db6c4 fix(#216): fixed issue where .env vars were not injected into bru.getEnvVar() 2023-09-28 04:32:07 +05:30
Anoop M D
c27f090583 feat(#95): runner runs inside a tab of a collection view 2023-09-28 04:02:20 +05:30
Anoop M D
487dd73040 fix: fixed screen crash when collection was removed 2023-09-28 03:26:13 +05:30
Anoop M D
665428a2d0 feat(#224): proxy support feature - gui layer 2023-09-28 03:06:53 +05:30
Mirko Golze
6a2ba0f746 try other way to retrieve icon path for about window 2023-09-27 22:39:22 +02:00
Mirko Golze
36f9902f2e #191 interpolate header names with variables 2023-09-27 22:36:27 +02:00
Anoop M D
c0b7dad030 feat(#224): proxy support feature - electron layer 2023-09-28 00:58:05 +05:30
Anoop M D
8780d309ac feat: exposing chai library in script and test runtimes 2023-09-27 23:47:56 +05:30
Anoop M D
08c1563a7a chore: bump version to v0.15.3 2023-09-27 14:37:13 +05:30
Anoop M D
07ad1f9f60 fix(#217): Merge pull request #218 from tpyle/bug/no-environments
Adds fallback when no environments are defined
2023-09-27 14:35:26 +05:30
Thomas Pyle
8df6b241bb Adds fallback when no environments are defined 2023-09-26 19:39:46 -04:00
Anoop M D
50e0558d7d Merge pull request #215 from Cibico99/feature/XML-Format
Feature/xml format
2023-09-26 22:58:33 +05:30
Anoop M D
cbe84cc512 Merge pull request #213 from BrentShikoski/feature/license_all_modules
Add license to published npm modules.
2023-09-26 22:50:18 +05:30
Anoop M D
cbb975d81d Merge branch 'main' into feature/license_all_modules 2023-09-26 22:49:19 +05:30
Anoop M D
30ee472c40 release(#212): bru cli v0.9.0 2023-09-26 22:37:55 +05:30
Anoop M D
c7aecbea79 Merge pull request #212 from tpyle/feature/output-collection
Adds an option to collect output from cli runs
2023-09-26 22:09:57 +05:30
pedward99
b814c84411 Clean Up 2023-09-26 09:22:41 -04:00
Brent Shikoski
6306ad17c3 Add license information to modules. 2023-09-25 20:57:51 -05:00
Brent Shikoski
4b800e30e4 Merge branch 'usebruno:main' into feature/add_license_to_all_cli_dependent_modules 2023-09-25 19:37:10 -05:00
Thomas Pyle
89f418a114 Adds an option to collect output from cli runs 2023-09-25 17:48:53 -04:00
pedward99
9c8ef09d01 XML Format working 2023-09-25 13:13:14 -04:00
Brent Shikoski
83d354c25c Add license to modules the cli is dependent on.
- bruno-js
- bruno-lang
- bruno-query
2023-09-24 22:29:10 -05:00
Anoop M D
bb31ddc5d2 chore: release v0.15.2 2023-09-25 04:42:41 +05:30
Anoop M D
ff40178c8c fix(#210): fixing bruno libraries dep issues 2023-09-25 04:41:39 +05:30
Anoop M D
1c549f7faf fix: fixed issue related about-window dep breaking build 2023-09-25 02:54:36 +05:30
Anoop M D
eb6b75ff98 feat(#199): bru cli updates to load .env vars 2023-09-25 02:10:12 +05:30
Anoop M D
eb010adeac chore: added collection variables feature note 2023-09-25 01:09:25 +05:30
Anoop M D
7e5e22cfcf chore: release v0.15.0 2023-09-25 00:58:24 +05:30
Anoop M D
2515e78a10 feat(#200): req.setMaxRedirects() api 2023-09-25 00:09:29 +05:30
Anoop M D
511854369f feat(#205): collection properties dropdown 2023-09-24 23:53:31 +05:30
Anoop M D
18f185d37c chore: fixed env table styling issue 2023-09-24 23:31:48 +05:30
Anoop M D
7a0322d09e Merge pull request #209 from usebruno/feature/env-secrets
Feature/env secrets
2023-09-24 23:22:40 +05:30
Anoop M D
2dadad3af0 Merge branch 'main' into feature/env-secrets 2023-09-24 23:11:45 +05:30
Anoop M D
eaa31342dc Merge pull request #207 from mirkogolze/feature/env-secrets
#199 improve code to check given env vars correctly
2023-09-24 23:07:08 +05:30
Anoop M D
c4fd9d38a5 Merge pull request #208 from mirkogolze/feature/cli-env-vars
#199 bring feature cli overridable env vars to main
2023-09-24 23:05:19 +05:30
Anoop M D
9c4c219b99 feat(#199): Env Secrets - UI and Electron Layer updates 2023-09-24 23:02:39 +05:30
Mirko Golze
8e22aa2fca #199 improve code to check given envvars correctly 2023-09-24 15:40:04 +02:00
Mirko Golze
6b9e085696 #199 small code refactoring 2023-09-24 15:39:56 +02:00
Mirko Golze
74282706aa #199 add CLI feature to use command line parameter '--env-var secret=xzy123' 2023-09-24 15:39:51 +02:00
Mirko Golze
aa88aa73a2 #199 improve code to check given envvars correctly 2023-09-24 15:28:33 +02:00
Anoop M D
f78c1640e9 feat(#199): electron safeStorage util for storing secrets with aes256 fallback 2023-09-24 17:49:28 +05:30
Anoop M D
a5a17cf8eb fix(#131): fixed macos ctrl+a select all issue 2023-09-24 02:47:05 +05:30
Anoop M D
c5a86cb343 feat: documentation link in app titlebar 2023-09-24 02:33:58 +05:30
Anoop M D
9b94cddc9b Merge pull request #204 from jeffprinty/enhancement/add-about-window
Issue #203 Add about-window to help menu
2023-09-24 02:10:23 +05:30
Anoop M D
0a172ddce8 feat(#206): Collection and Env variables viewer 2023-09-24 02:07:31 +05:30
Jeff Printy
aea1cbba9e Issue #203 Add about-window to help menu 2023-09-22 23:17:53 -05:00
Anoop M D
7a1b44858d Merge pull request #202 from mirkogolze/feature/env-secrets
#199 add CLI feature to use command line parameter '--env-var' secret=…
2023-09-23 03:18:24 +05:30
Anoop M D
1c89ab3450 fix: fixed issue where vm2 instantiated objects were not being sent to renderer process 2023-09-23 03:14:27 +05:30
Anoop M D
e3ce420216 feat(#122): supporting process.env vars in UI and electron layer 2023-09-23 02:55:54 +05:30
Anoop M D
c91fef2264 chore: refactor electron storage related modules 2023-09-22 20:38:45 +05:30
Mirko Golze
c83fce16dc #199 small code refactoring 2023-09-22 09:22:46 +02:00
Mirko Golze
5415e20d7e #199 add CLI feature to use command line parameter '--env-var secret=xzy123' 2023-09-21 22:17:46 +02:00
Anoop M D
2f45b95930 feat(#199): bru lang updates to store environment secrets 2023-09-22 01:08:35 +05:30
Anoop M D
4531cfc994 chore(#197): ran prettier on tests folder 2023-09-22 00:45:42 +05:30
Anoop M D
bd0738198c chore(#197): ran prettier on packages/bruno-testbench 2023-09-22 00:44:47 +05:30
Anoop M D
9a81793151 chore(#197): ran prettier on packages/bruno-tauri 2023-09-22 00:44:28 +05:30
Anoop M D
88c16fa388 chore(#197): ran prettier on packages/bruno-schema 2023-09-22 00:43:56 +05:30
Anoop M D
f68eacfe0d chore(#197): ran prettier on packages/bruno-query 2023-09-22 00:43:31 +05:30
Anoop M D
116e050987 chore(#197): ran prettier on packages/bruno-lang 2023-09-22 00:42:48 +05:30
Anoop M D
5af2f68252 chore(#197): ran prettier on packages/bruno-js 2023-09-22 00:42:14 +05:30
Anoop M D
a53dd76854 chore(#197): ran prettier on packages/bruno-graphql-docs 2023-09-22 00:37:51 +05:30
Anoop M D
67fe264494 chore(#197): ran prettier on packages/bruno-electron 2023-09-22 00:36:42 +05:30
Anoop M D
ae692dde06 chore(#197): ran prettier on packages/bruno-cli 2023-09-22 00:34:11 +05:30
Anoop M D
1c4c5cc0c0 feat(#122): Support parsing of dotenv files 2023-09-22 00:27:27 +05:30
Anoop M D
19ca1af71e chore: release 0.14.1 2023-09-20 13:11:16 +05:30
Anoop M D
4016a83626 Merge pull request #201 from mirkogolze/bugfix/import-postman-header-check
#192 implement fallback to search body language by header 'content-type'
2023-09-20 13:04:25 +05:30
Mirko Golze
71b18c8b21 implement fallback to search body language by content-type header 2023-09-20 08:31:23 +02:00
Anoop M D
b53fcbb3d1 Merge pull request #198 from usebruno/feature/prettier-formatting
feat(#197): prettier formatting on all files in packages/bruno-app
2023-09-18 13:47:05 +05:30
Paul Edwards
aeb29393c5 Code Editor Mode and formatting 2023-07-19 20:06:37 -04:00
Paul Edwards
0866d33858 Merge branch 'main' of https://github.com/usebruno/bruno into main 2023-05-17 00:09:23 -04:00
Paul Edwards
ad905d1a0a XML Indenting with header check 2023-04-26 22:06:52 -04:00
202 changed files with 5529 additions and 2881 deletions

View File

@@ -1,29 +1,31 @@
name: Unit Tests
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
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: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Build Package bruno-query
run: npm run build --workspace=packages/bruno-query
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Build Package bruno-query
run: npm run build --workspace=packages/bruno-query
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron

4
.gitignore vendored
View File

@@ -41,3 +41,7 @@ yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
#dev editor
bruno.iml
.idea

View File

@@ -1,9 +1,10 @@
## Development
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
### Dependencies
* NodeJS v18
- NodeJS v18
### Local Development
@@ -15,7 +16,6 @@ nvm use
npm i --legacy-peer-deps
# build graphql docs
# note: you can for now ignore the error thrown while building the graphql docs
npm run build:graphql-docs
# build bruno query

View File

@@ -13,6 +13,7 @@
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
@@ -37,5 +38,6 @@
},
"overrides": {
"rollup": "3.2.5"
}
},
"dependencies": {}
}

View File

@@ -18,7 +18,7 @@
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.3.1",
"@usebruno/schema": "0.5.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -32,6 +32,7 @@
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
@@ -47,11 +48,13 @@
"react-dom": "18.2.0",
"react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0",
"react-inspector": "^6.0.2",
"react-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"xml-formatter": "^3.5.0",
"yup": "^0.32.11"
},
"devDependencies": {

View File

@@ -29,7 +29,7 @@ const BrunoSupport = ({ onClose }) => {
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
<span className="label ml-2">GitHub</span>
</a>
</div>
<div className="mt-2">

View File

@@ -80,7 +80,7 @@ export default class CodeEditor extends React.Component {
}
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -0,0 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,190 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import StyledWrapper from './StyledWrapper';
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const formik = useFormik({
initialValues: {
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
},
validationSchema: Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https']),
hostname: Yup.string().max(1024),
port: Yup.number().min(0).max(65535),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
})
}),
onSubmit: (values) => {
onUpdate(values);
}
});
useEffect(() => {
formik.setValues({
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
});
}, [proxyConfig]);
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center mr-4">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
http
</label>
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
https
</label>
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<input
id="auth.password"
type="text"
name="auth.password"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default ProxySettings;

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
table {
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
li {
background-color: ${(props) => props.theme.bg} !important;
}
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
import StyledWrapper from './StyledWrapper';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
return (
<StyledWrapper className="px-4 py-4">
<h1 className="font-semibold mb-4">Collection Settings</h1>
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
</StyledWrapper>
);
};
export default CollectionSettings;

View File

@@ -43,7 +43,7 @@ const Wrapper = styled.div`
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
}
}
}

View File

@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occured while created the environment'));
.catch(() => toast.error('An error occurred while created the environment'));
}
});

View File

@@ -14,7 +14,7 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
toast.success('Environment deleted successfully');
onClose();
})
.catch(() => toast.error('An error occured while deleting the environment'));
.catch(() => toast.error('An error occurred while deleting the environment'));
};
return (

View File

@@ -5,10 +5,22 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
padding: 4px 10px;
&:nth-child(1),
&:nth-child(4),
&:nth-child(5) {
width: 70px;
}
&:nth-child(2) {
width: 25%;
}
}
thead {
@@ -16,7 +28,7 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
user-select: none;
}
td {
thead td {
padding: 6px 10px;
}
}

View File

@@ -2,13 +2,16 @@ import React, { useReducer } from 'react';
import toast from 'react-hot-toast';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import reducer from './reducer';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
const { variables, hasChanges } = state;
@@ -20,7 +23,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
type: 'CHANGES_SAVED'
});
})
.catch(() => toast.error('An error occured while saving the changes'));
.catch(() => toast.error('An error occurred while saving the changes'));
};
const addVariable = () => {
@@ -44,6 +47,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
variable.enabled = e.target.checked;
break;
}
case 'secret': {
variable.secret = e.target.checked;
break;
}
}
reducerDispatch({
type: 'UPDATE_VAR',
@@ -63,8 +70,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
<table>
<thead>
<tr>
<td>Enabled</td>
<td>Name</td>
<td>Value</td>
<td>Secret</td>
<td></td>
</tr>
</thead>
@@ -73,6 +82,14 @@ const EnvironmentVariables = ({ environment, collection }) => {
? variables.map((variable, index) => {
return (
<tr key={variable.uid}>
<td className="text-center">
<input
type="checkbox"
checked={variable.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, variable, 'enabled')}
/>
</td>
<td>
<input
type="text"
@@ -86,29 +103,25 @@ const EnvironmentVariables = ({ environment, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={variable.value}
className="mousetrap"
onChange={(e) => handleVarChange(e, variable, 'value')}
theme={storedTheme}
onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={variable.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, variable, 'enabled')}
/>
<button onClick={() => handleRemoveVars(variable)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
<input
type="checkbox"
checked={variable.secret}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, variable, 'secret')}
/>
</td>
<td>
<button onClick={() => handleRemoveVars(variable)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
);

View File

@@ -12,6 +12,7 @@ const reducer = (state, action) => {
name: '',
value: '',
type: 'text',
secret: false,
enabled: true
});
draft.hasChanges = true;
@@ -24,6 +25,7 @@ const reducer = (state, action) => {
variable.name = action.variable.name;
variable.value = action.variable.value;
variable.enabled = action.variable.enabled;
variable.secret = action.variable.secret;
draft.hasChanges = true;
});
}

View File

@@ -27,7 +27,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
toast.success('Environment renamed successfully');
onClose();
})
.catch(() => toast.error('An error occured while renaming the environment'));
.catch(() => toast.error('An error occurred while renaming the environment'));
}
});

View File

@@ -19,7 +19,7 @@ const Wrapper = styled.div`
align-items: flex-start;
justify-content: center;
overflow-y: auto;
z-index: 1003;
z-index: 10;
}
.bruno-modal-card {
@@ -28,7 +28,7 @@ const Wrapper = styled.div`
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
z-index: 1003;
z-index: 10;
max-width: calc(100% - var(--spacing-base-unit));
box-shadow: var(--box-shadow-base);
display: flex;

View File

@@ -3,7 +3,7 @@ import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel }) => (
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
{title ? <div className="bruno-modal-header-title">{title}</div> : null}
{handleCancel ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×

View File

@@ -27,7 +27,7 @@ const Support = () => {
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
<span className="label ml-2">GitHub</span>
</a>
</div>
<div className="mt-2">

View File

@@ -114,7 +114,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
@@ -125,7 +125,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
Query
</div>

View File

@@ -40,7 +40,7 @@ const useGraphqlSchema = (endpoint, environment) => {
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error('Error occured while loading GraphQL Schema');
toast.error('Error occurred while loading GraphQL Schema');
});
};

View File

@@ -62,7 +62,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
@@ -73,7 +73,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Query
</div>

View File

@@ -142,7 +142,7 @@ export default class QueryEditor extends React.Component {
}
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -72,19 +74,28 @@ const RequestHeaders = ({ item, collection }) => {
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
? headers.map((header) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'name'
)
}
autocomplete={headerAutoCompleteList}
onRun={handleRun}
collection={collection}
/>
</td>
<td>

View File

@@ -13,6 +13,8 @@ import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import RunnerResults from 'components/RunnerResults';
import VariablesEditor from 'components/VariablesEditor';
import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
@@ -110,7 +112,7 @@ const RequestTabPanel = () => {
}
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
@@ -118,11 +120,18 @@ const RequestTabPanel = () => {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const showRunner = collection.showRunner;
if (showRunner) {
if (focusedTab.type === 'collection-runner') {
return <RunnerResults collection={collection} />;
}
if (focusedTab.type === 'variables') {
return <VariablesEditor collection={collection} />;
}
if (focusedTab.type === 'collection-settings') {
return <CollectionSettings collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { IconFiles, IconRun } from '@tabler/icons';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import VariablesView from 'components/VariablesView';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({ collection }) => {
@@ -11,8 +11,30 @@ const CollectionToolBar = ({ collection }) => {
const handleRun = () => {
dispatch(
toggleRunnerView({
collectionUid: collection.uid
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
};
const viewVariables = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'variables'
})
);
};
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
@@ -28,7 +50,12 @@ const CollectionToolBar = ({ collection }) => {
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>
<VariablesView collection={collection} />
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</span>
<span className="mr-3">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
</span>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type }) => {
const getTabInfo = (type) => {
switch (type) {
case 'collection-settings': {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Settings</span>
</>
);
}
case 'variables': {
return (
<>
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Variables</span>
</>
);
}
case 'collection-runner': {
return (
<>
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Runner</span>
</>
);
}
}
};
return (
<>
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
</>
);
};
export default SpecialTab;

View File

@@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
@@ -56,6 +57,14 @@ const RequestTab = ({ tab, collection }) => {
return color;
};
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
</StyledWrapper>
);
}
const item = findItemInCollection(collection, tab.uid);
if (!item) {

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
border-bottom: 1px solid ${(props) => props.theme.requestTabs.borromBorder};
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
ul {
padding: 0;

View File

@@ -76,9 +76,7 @@ const RequestTabs = () => {
});
};
const showRunner = activeCollection && activeCollection.showRunner;
// Todo: Must support ephermal requests
// Todo: Must support ephemeral requests
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && (
@@ -87,72 +85,70 @@ const RequestTabs = () => {
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection} />
{!showRunner ? (
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li
key={tab.uid}
className={getTabClassname(tab, index)}
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li
key={tab.uid}
className={getTabClassname(tab, index)}
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li> */}
</ul>
</div>
) : null}
</li>
) : null}
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
</div>
</li>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</div>
</li> */}
</ul>
</div>
</>
) : null}
</StyledWrapper>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { safeStringifyJSON } from 'utils/common';
import { useDispatch, useSelector } from 'react-redux';
import { getContentType, formatResponse } from 'utils/common';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
@@ -41,9 +41,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
item={item}
collection={collection}
width={rightPaneWidth}
value={
response.data ? (isJson(response.headers) ? safeStringifyJSON(response.data, true) : response.data) : ''
}
value={response.data ? formatResponse(response) : ''}
mode={getContentType(response.headers)}
/>
);
@@ -95,31 +93,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
});
};
const getContentType = (headers) => {
if (headers && headers.length) {
let contentType = headers
.filter((header) => header[0].toLowerCase() === 'content-type')
.map((header) => {
return header[1];
});
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
}
}
return '';
};
const isJson = (headers) => {
return getContentType(headers) === 'application/ld+json';
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center px-3 tabs" role="tablist">
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
Response
</div>

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { useDispatch } from 'react-redux';
import { get, each, cloneDeep } from 'lodash';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
import slash from 'utils/common/slash';
@@ -69,9 +69,9 @@ export default function RunnerResults({ collection }) {
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
};
const closeRunner = () => {
const resetRunner = () => {
dispatch(
closeCollectionRunner({
resetCollectionRunner({
collectionUid: collection.uid
})
);
@@ -101,8 +101,8 @@ export default function RunnerResults({ collection }) {
Run Collection
</button>
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
Close
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
Reset
</button>
</StyledWrapper>
);
@@ -202,8 +202,8 @@ export default function RunnerResults({ collection }) {
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
Run Collection
</button>
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
Close
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
Reset
</button>
</div>
) : null}

View File

@@ -28,7 +28,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
onClose();
})
.catch((err) => {
toast.error(err ? err.message : 'An error occured while cloning the request');
toast.error(err ? err.message : 'An error occurred while cloning the request');
});
}
});

View File

@@ -1,9 +1,10 @@
import React from 'react';
import get from 'lodash/get';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
@@ -12,8 +13,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const onSubmit = (recursive) => {
dispatch(
showRunnerView({
collectionUid: collection.uid
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));

View File

@@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -86,11 +86,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
const handleClick = (event) => {
dispatch(
hideRunnerView({
collectionUid: collection.uid
})
);
if (isItemARequest(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(

View File

@@ -0,0 +1,50 @@
import React from 'react';
import Modal from 'components/Modal';
function countRequests(items) {
let count = 0;
function recurse(item) {
if (item && typeof item === 'object') {
if (item.type !== 'folder') {
count++;
}
if (Array.isArray(item.items)) {
item.items.forEach(recurse);
}
}
}
items.forEach(recurse);
return count;
}
const CollectionProperties = ({ collection, onClose }) => {
return (
<Modal size="sm" title="Collection Properties" hideFooter={true} handleCancel={onClose}>
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-right">Name&nbsp;:</td>
<td className="py-2 px-2">{collection.name}</td>
</tr>
<tr className="">
<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">Environments&nbsp;:</td>
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Requests&nbsp;:</td>
<td className="py-2 px-2">{countRequests(collection.items)}</td>
</tr>
</tbody>
</table>
</Modal>
);
};
export default CollectionProperties;

View File

@@ -13,7 +13,7 @@ const RemoveCollection = ({ onClose, collection }) => {
toast.success('Collection removed');
onClose();
})
.catch(() => toast.error('An error occured while removing the collection'));
.catch(() => toast.error('An error occurred while removing the collection'));
};
return (

View File

@@ -1,5 +1,6 @@
import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import { useDrop } from 'react-dnd';
@@ -8,11 +9,12 @@ import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import RunCollectionItem from './CollectionItem/RunCollectionItem';
import CollectionProperties from './CollectionProperties';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
import exportCollection from 'utils/collections/export';
@@ -25,7 +27,7 @@ const Collection = ({ collection, searchText }) => {
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [showRunCollectionModal, setShowRunCollectionModal] = useState(false);
const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false);
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
const dispatch = useDispatch();
@@ -39,6 +41,16 @@ const Collection = ({ collection, searchText }) => {
);
});
const handleRun = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
};
useEffect(() => {
if (searchText && searchText.length) {
setCollectionIsCollapsed(false);
@@ -103,8 +115,8 @@ const Collection = ({ collection, searchText }) => {
{showRemoveCollectionModal && (
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
)}
{showRunCollectionModal && (
<RunCollectionItem collection={collection} onClose={() => setShowRunCollectionModal(false)} />
{collectionPropertiesModal && (
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
)}
<div className="flex py-1 collection-name items-center" ref={drop}>
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
@@ -142,7 +154,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowRunCollectionModal(true);
handleRun();
}}
>
Run
@@ -165,6 +177,15 @@ const Collection = ({ collection, searchText }) => {
>
Export
</div>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setCollectionPropertiesModal(true);
}}
>
Properties
</div>
<div
className="dropdown-item"
onClick={(e) => {

View File

@@ -19,7 +19,7 @@ const CreateOrOpenCollection = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};
const CreateLink = () => (

View File

@@ -1,21 +1,28 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IconSearch, IconFolders } from '@tabler/icons';
import { useDispatch, useSelector } from 'react-redux';
import { IconSearch, IconFolders, IconSortAZ } from '@tabler/icons';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
import CreateOrOpenCollection from './CreateOrOpenCollection';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
const CollectionsBadge = () => {
const dispatch = useDispatch()
return (
<div className="items-center mt-2 relative">
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
<span className="mr-2">
<IconFolders size={18} strokeWidth={1.5} />
</span>
<span>Collections</span>
<div className='collections-badge flex items-center justify-between px-2' >
<div className="flex items-center py-1 select-none">
<span className="mr-2">
<IconFolders size={18} strokeWidth={1.5} />
</span>
<span>Collections</span>
</div>
<button onClick={() => dispatch(sortCollections())} >
<IconSortAZ size={18} strokeWidth={1.5} />
</button>
</div>
</div>
);
@@ -64,12 +71,12 @@ const Collections = () => {
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
{collections && collections.length
? collections.map((c) => {
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
: null}
</div>
</StyledWrapper>

View File

@@ -27,8 +27,11 @@ const CreateCollection = ({ onClose }) => {
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string().required('location is required')
collectionLocation: Yup.string()
.min(1, 'location is required')
.required('location is required'),
}),
onSubmit: (values) => {
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
@@ -36,14 +39,17 @@ const CreateCollection = ({ onClose }) => {
toast.success('Collection created');
onClose();
})
.catch(() => toast.error('An error occured while creating the collection'));
.catch(() => toast.error('An error occurred while creating the collection'));
}
});
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
formik.setFieldValue('collectionLocation', dirPath);
// When the user closes the diolog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -63,9 +69,8 @@ const CreateCollection = ({ onClose }) => {
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="collectionName" className="flex items-center">
<span className="font-semibold">Name</span>
<Tooltip text="Name of the collection" tooltipId="collection-name" />
<label htmlFor="collection-name" className="flex items-center font-semibold">
Name
</label>
<input
id="collection-name"
@@ -84,9 +89,34 @@ const CreateCollection = ({ onClose }) => {
<div className="text-red-500">{formik.errors.collectionName}</div>
) : null}
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
<label htmlFor="collection-location" className="block font-semibold mt-3">
Location
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
readOnly={true}
className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
/>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name" />
<Tooltip text="This folder will be created under the selected location" tooltipId="collection-folder-name-tooltip" />
</label>
<input
id="collection-folder-name"
@@ -103,34 +133,6 @@ const CreateCollection = ({ onClose }) => {
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
<>
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
readOnly={true}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
/>
</>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
</div>
</form>
</Modal>

View File

@@ -32,7 +32,7 @@ const NewFolder = ({ collection, item, onClose }) => {
onSubmit: (values) => {
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
}
});

View File

@@ -5,14 +5,14 @@ import toast from 'react-hot-toast';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
@@ -34,10 +34,10 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
})
}),
onSubmit: (values) => {
if (isEphermal) {
if (isEphemeral) {
const uid = uuid();
dispatch(
newEphermalHttpRequest({
newEphemeralHttpRequest({
uid: uid,
requestName: values.requestName,
requestType: values.requestType,
@@ -56,7 +56,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
);
onClose();
})
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else {
dispatch(
newHttpRequest({
@@ -69,7 +69,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
})
)
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
}
}
});

View File

@@ -46,7 +46,7 @@ const TitleBar = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};

View File

@@ -116,7 +116,7 @@ const Sidebar = () => {
</GitHubButton>
)}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.14.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.6</div>
</div>
</div>
</div>

View File

@@ -19,6 +19,7 @@ const StyledWrapper = styled.div`
.CodeMirror-scroll {
overflow: hidden !important;
padding-bottom: 50px !important;
}
.CodeMirror-hscrollbar {

View File

@@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
const word = /[\w$-]+/;
const wordlist = (options && options.autocomplete) || [];
let cur = editor.getCursor(),
curLine = editor.getLine(cur.line);
let end = cur.ch,
start = end;
while (start && word.test(curLine.charAt(start - 1))) --start;
let curWord = start != end && curLine.slice(start, end);
// Check if curWord is a valid string before proceeding
if (typeof curWord !== 'string' || curWord.length < 3) {
return null; // Abort the hint
}
const list = (options && options.list) || [];
const re = new RegExp(word.source, 'g');
for (let dir = -1; dir <= 1; dir += 2) {
let line = cur.line,
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
for (; line != endLine; line += dir) {
let text = editor.getLine(line),
m;
while ((m = re.exec(text))) {
if (line == cur.line && curWord.length < 3) continue;
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
}
}
}
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
});
CodeMirror.commands.autocomplete = (cm, hint, options) => {
cm.showHint({ hint, ...options });
};
}
class SingleLineEditor extends Component {
@@ -31,6 +65,7 @@ class SingleLineEditor extends Component {
brunoVarInfo: {
variables: getAllVariables(this.props.collection)
},
scrollbarStyle: null,
extraKeys: {
Enter: () => {
if (this.props.onRun) {
@@ -72,6 +107,14 @@ class SingleLineEditor extends Component {
Tab: () => {}
}
});
if (this.props.autocomplete) {
this.editor.on('keyup', (cm, event) => {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
/*Enter - do not open autocomplete list just after item has been selected in it*/
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
}
});
}
this.editor.setValue(this.props.value || '');
this.editor.on('change', this._onEdit);
this.addOverlay();
@@ -87,7 +130,7 @@ class SingleLineEditor extends Component {
};
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
table {
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
li {
background-color: ${(props) => props.theme.bg} !important;
}
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,98 @@
import React from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { Inspector } from 'react-inspector';
import { useTheme } from 'providers/Theme';
import { findEnvironmentInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const KeyValueExplorer = ({ data, theme }) => {
data = data || {};
return (
<div>
<table className="border-collapse">
<tbody>
{Object.entries(data).map(([key, value]) => (
<tr key={key}>
<td className="px-2 py-1">{key}</td>
<td className="px-2 py-1">
<Inspector data={value} theme={theme} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const EnvVariables = ({ collection, theme }) => {
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if (!environment) {
return (
<>
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
<div className="muted text-xs">No environment selected</div>
</>
);
}
const envVars = get(environment, 'variables', []);
const enabledEnvVars = filter(envVars, (variable) => variable.enabled);
const envVarsObj = enabledEnvVars.reduce((acc, curr) => {
acc[curr.name] = curr.value;
return acc;
}, {});
return (
<>
<div className="flex items-center mt-4 mb-2">
<h1 className="font-semibold">Environment Variables</h1>
<span className="muted ml-2">({environment.name})</span>
</div>
{enabledEnvVars.length > 0 ? (
<KeyValueExplorer data={envVarsObj} theme={theme} />
) : (
<div className="muted text-xs">No environment variables found</div>
)}
</>
);
};
const CollectionVariables = ({ collection, theme }) => {
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
return (
<>
<h1 className="font-semibold mb-2">Collection Variables</h1>
{collectionVariablesFound ? (
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
) : (
<div className="muted text-xs">No collection variables found</div>
)}
</>
);
};
const VariablesEditor = ({ collection }) => {
const { storedTheme } = useTheme();
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
return (
<StyledWrapper className="px-4 py-4">
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
<EnvVariables collection={collection} theme={reactInspectorTheme} />
<div className="mt-8 muted text-xs">
Note: As of today, collection variables can only be set via the API -{' '}
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
In the next release, we will add a UI to set and modify collection variables.
</div>
</StyledWrapper>
);
};
export default VariablesEditor;

View File

@@ -1,19 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: absolute;
min-width: fit-content;
font-size: 14px;
top: 36px;
right: 0;
white-space: nowrap;
z-index: 1000;
background-color: ${(props) => props.theme.variables.bg};
.popover {
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
`;
export default Wrapper;

View File

@@ -1,26 +0,0 @@
import React, { useRef } from 'react';
import StyledWrapper from './StyledWrapper';
import useOnClickOutside from 'hooks/useOnClickOutside';
const PopOver = ({ children, iconRef, handleClose }) => {
const popOverRef = useRef(null);
useOnClickOutside(popOverRef, (e) => {
if (iconRef && iconRef.current) {
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
return;
}
}
handleClose();
});
return (
<StyledWrapper>
<div className="popover" ref={popOverRef}>
<div className="popover-content">{children}</div>
</div>
</StyledWrapper>
);
};
export default PopOver;

View File

@@ -1,15 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
align-self: stretch;
display: flex;
align-items: center;
.view-environment {
width: 1rem;
font-size: 10px;
}
`;
export default StyledWrapper;

View File

@@ -1,19 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.variable-name {
color: ${(props) => props.theme.variables.name.color};
}
.variable-name {
min-width: 180px;
}
.variable-value {
max-width: 600px;
inline-size: 600px;
overflow-wrap: break-word;
}
`;
export default StyledWrapper;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import StyledWrapper from './StyledWrapper';
const VariablesTable = ({ variables, collectionVariables }) => {
const collectionVars = [];
forOwn(cloneDeep(collectionVariables), (value, key) => {
collectionVars.push({
uid: uuid(),
name: key,
value: value
});
});
return (
<StyledWrapper>
<div className="flex flex-col w-full">
<div className="mb-2 font-medium">Environment Variables</div>
{variables && variables.length ? (
variables.map((variable) => {
return (
<div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
</div>
);
})
) : (
<small>No env variables found</small>
)}
<div className="mt-2 font-medium">Collection Variables</div>
{collectionVars && collectionVars.length ? (
collectionVars.map((variable) => {
return (
<div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
</div>
);
})
) : (
<small>No collection variables found</small>
)}
</div>
</StyledWrapper>
);
};
export default VariablesTable;

View File

@@ -1,48 +0,0 @@
import React, { useState, useRef } from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { findEnvironmentInCollection } from 'utils/collections';
import VariablesTable from './VariablesTable';
import StyledWrapper from './StyledWrapper';
import PopOver from './Popover';
import { IconEye } from '@tabler/icons';
const VariablesView = ({ collection }) => {
const iconRef = useRef(null);
const [popOverOpen, setPopOverOpen] = useState(false);
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const variables = get(environment, 'variables', []);
const enabledVariables = filter(variables, (variable) => variable.enabled);
const showVariablesTable =
enabledVariables.length > 0 ||
(collection.collectionVariables && Object.keys(collection.collectionVariables).length > 0);
return (
<StyledWrapper className="mr-2 server-syncstatus-icon" ref={iconRef}>
<div
className="flex p-1 items-center"
onClick={() => setPopOverOpen(true)}
onMouseEnter={() => setPopOverOpen(true)}
onMouseLeave={() => setPopOverOpen(false)}
>
<div className="cursor-pointer view-environment">
<IconEye size={18} strokeWidth={1.5} />
</div>
{popOverOpen && (
<PopOver iconRef={iconRef} handleClose={() => setPopOverOpen(false)}>
<div className="px-2 py-1">
{showVariablesTable ? (
<VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables} />
) : (
'No variables found'
)}
</div>
</PopOver>
)}
</div>
</StyledWrapper>
);
};
export default VariablesView;

View File

@@ -19,7 +19,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};
@@ -54,7 +54,7 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
<div className="uppercase font-semibold heading mt-10">Collections</div>
<div className="mt-4 flex items-center collection-options select-none">
@@ -93,7 +93,7 @@ const Welcome = () => {
<div className="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>
<span className="label ml-2">GitHub</span>
</a>
</div>
</div>

View File

@@ -0,0 +1,44 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidMount() {
// Add a global error event listener to capture client-side errors
window.onerror = (message, source, lineno, colno, error) => {
this.setState({ hasError: true, error });
};
}
componentDidCatch(error, errorInfo) {
console.log({ error, errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center p-10">
<div className="bg-white rounded-lg shadow-lg p-4 w-full">
<h1 className="text-2xl font-semibold text-red-600 mb-2">Oops! Something went wrong</h1>
<p className="text-red-600 mb-2">{this.state.error && this.state.error.toString()}</p>
{this.state.error && this.state.error.stack && (
<pre className="bg-gray-100 p-2 rounded-lg overflow-auto">{this.state.error.stack}</pre>
)}
<button
className="bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition"
onClick={() => {
this.setState({ hasError: false, error: null });
}}
>
Close
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
import ReduxStore from 'providers/ReduxStore';
import ThemeProvider from 'providers/Theme/index';
import ErrorBoundary from './ErrorBoundary';
import '../styles/app.scss';
import '../styles/globals.css';
@@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) {
}
return (
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<PreferencesProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</PreferencesProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>
</SafeHydrate>
<ErrorBoundary>
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<PreferencesProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</PreferencesProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>
</SafeHydrate>
</ErrorBoundary>
);
}

View File

@@ -8,9 +8,11 @@ import {
collectionUnlinkDirectoryEvent,
collectionUnlinkEnvFileEvent,
scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
collectionRenamedEvent,
runRequestEvent,
runFolderEvent
runFolderEvent,
brunoConfigUpdateEvent
} from 'providers/ReduxStore/slices/collections';
import toast from 'react-hot-toast';
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
@@ -26,8 +28,8 @@ const useCollectionTreeSync = () => {
const { ipcRenderer } = window;
const _openCollection = (pathname, uid, name) => {
dispatch(openCollectionEvent(uid, pathname, name));
const _openCollection = (pathname, uid, brunoConfig) => {
dispatch(openCollectionEvent(uid, pathname, brunoConfig));
};
const _collectionTreeUpdated = (type, val) => {
@@ -97,6 +99,10 @@ const useCollectionTreeSync = () => {
dispatch(scriptEnvironmentUpdateEvent(val));
};
const _processEnvUpdate = (val) => {
dispatch(processEnvUpdateEvent(val));
};
const _collectionRenamed = (val) => {
dispatch(collectionRenamedEvent(val));
};
@@ -119,9 +125,11 @@ const useCollectionTreeSync = () => {
const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent);
const removeListener9 = ipcRenderer.on('main:console-log', (val) => {
const removeListener9 = ipcRenderer.on('main:process-env-update', _processEnvUpdate);
const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args);
});
const removeListener11 = ipcRenderer.on('main:bruno-config-update', (val) => dispatch(brunoConfigUpdateEvent(val)));
return () => {
removeListener1();
@@ -133,6 +141,8 @@ const useCollectionTreeSync = () => {
removeListener7();
removeListener8();
removeListener9();
removeListener10();
removeListener11();
};
}, [isElectron]);
};

View File

@@ -93,7 +93,7 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid, tabs, saveRequest, collections]);
// edit environmentss (ctrl/cmd + e)
// edit environments (ctrl/cmd + e)
useEffect(() => {
Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);

View File

@@ -22,7 +22,7 @@ import {
} from 'utils/collections';
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common';
import { getDirectoryName } from 'utils/common/platform';
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
@@ -34,6 +34,7 @@ import {
renameItem as _renameItem,
cloneItem as _cloneItem,
deleteItem as _deleteItem,
sortCollections as _sortCollections,
saveRequest as _saveRequest,
selectEnvironment as _selectEnvironment,
createCollection as _createCollection,
@@ -42,8 +43,8 @@ import {
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
} from './index';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
const PATH_SEPARATOR = path.sep;
@@ -262,7 +263,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
ipcRenderer
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
.then(() => {
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
// But in windows we don't get those events, so we need to update the state manually
// This looks like an issue in our watcher library chokidar
// GH: https://github.com/usebruno/bruno/issues/251
if (isWindowsOS()) {
dispatch(_renameItem({ newName, itemUid, collectionUid }));
}
resolve();
})
.catch(reject);
});
};
@@ -346,13 +359,25 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type)
.then(() => resolve())
.then(() => {
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
// But in windows we don't get those events, so we need to update the state manually
// This looks like an issue in our watcher library chokidar
// GH: https://github.com/usebruno/bruno/issues/265
if (isWindowsOS()) {
dispatch(_deleteItem({ itemUid, collectionUid }));
}
resolve();
})
.catch((error) => reject(error));
}
return;
});
};
export const sortCollections = () => (dispatch) => {
dispatch(_sortCollections());
};
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -723,11 +748,7 @@ export const removeCollection = (collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:remove-collection', collection.pathname)
.then(() => {
dispatch(
closeTabs({
tabUids: recursivelyGetAllItemUids(collection.items)
})
);
dispatch(closeAllCollectionTabs({ collectionUid }));
})
.then(waitForNextTick)
.then(() => {
@@ -750,15 +771,31 @@ export const browseDirectory = () => (dispatch, getState) => {
});
};
export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState) => {
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
return reject(new Error('Collection not found'));
}
return new Promise((resolve, reject) => {
ipcRenderer
.invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid)
.then(resolve)
.catch(reject);
});
};
export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {
const collection = {
version: '1',
uid: uid,
name: name,
name: brunoConfig.name,
pathname: pathname,
items: [],
showRunner: false,
collectionVariables: {}
collectionVariables: {},
brunoConfig: brunoConfig
};
return new Promise((resolve, reject) => {

View File

@@ -52,6 +52,14 @@ export const collectionsSlice = createSlice({
state.collections.push(collection);
}
},
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.brunoConfig = brunoConfig;
}
},
renameCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -62,6 +70,9 @@ export const collectionsSlice = createSlice({
removeCollection: (state, action) => {
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
},
sortCollections: (state) => {
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name))
},
updateLastAction: (state, action) => {
const { collectionUid, lastAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -177,6 +188,14 @@ export const collectionsSlice = createSlice({
collection.collectionVariables = collectionVariables;
}
},
processEnvUpdateEvent: (state, action) => {
const { collectionUid, processEnvVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.processEnvVariables = processEnvVariables;
}
},
requestCancelled: (state, action) => {
const { itemUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -213,7 +232,7 @@ export const collectionsSlice = createSlice({
}
}
},
newEphermalHttpRequest: (state, action) => {
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection && collection.items && collection.items.length) {
@@ -1000,30 +1019,6 @@ export const collectionsSlice = createSlice({
collection.name = newName;
}
},
toggleRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = !collection.showRunner;
}
},
showRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = true;
}
},
hideRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = false;
}
},
resetRunResults: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -1133,13 +1128,12 @@ export const collectionsSlice = createSlice({
}
}
},
closeCollectionRunner: (state, action) => {
resetCollectionRunner: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.runnerResult = null;
collection.showRunner = false;
}
}
}
@@ -1147,8 +1141,10 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
brunoConfigUpdateEvent,
renameCollection,
removeCollection,
sortCollections,
updateLastAction,
collectionUnlinkEnvFileEvent,
saveEnvironment,
@@ -1158,10 +1154,11 @@ export const {
renameItem,
cloneItem,
scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
requestCancelled,
responseReceived,
saveRequest,
newEphermalHttpRequest,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
@@ -1198,13 +1195,10 @@ export const {
collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent,
collectionRenamedEvent,
toggleRunnerView,
showRunnerView,
hideRunnerView,
resetRunResults,
runRequestEvent,
runFolderEvent,
closeCollectionRunner
resetCollectionRunner
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@@ -10,6 +10,10 @@ const initialState = {
activeTabUid: null
};
const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
};
export const tabsSlice = createSlice({
name: 'tabs',
initialState,
@@ -19,12 +23,22 @@ export const tabsSlice = createSlice({
if (alreadyExists) {
return;
}
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;
return;
}
}
state.tabs.push({
uid: action.payload.uid,
collectionUid: action.payload.collectionUid,
requestPaneWidth: null,
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response'
responsePaneTab: 'response',
type: action.payload.type || 'request'
});
state.activeTabUid = action.payload.uid;
},
@@ -55,16 +69,22 @@ export const tabsSlice = createSlice({
closeTabs: (state, action) => {
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
const tabUids = action.payload.tabUids || [];
// remove the tabs from the state
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
if (activeTab && state.tabs.length) {
const { collectionUid } = activeTab;
const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
// if the active tab no longer exists, set the active tab to the last tab in the list
// this implies that the active tab was closed
if (!activeTabStillExists) {
// attempt to load sibling tabs (based on collections) of the dead tab
// load sibling tabs of the current collection
const siblingTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);
// if there are sibling tabs, set the active tab to the last sibling tab
// otherwise, set the active tab to the last tab in the list
if (siblingTabs && siblingTabs.length) {
state.activeTabUid = last(siblingTabs).uid;
} else {
@@ -76,11 +96,23 @@ export const tabsSlice = createSlice({
if (!state.tabs || !state.tabs.length) {
state.activeTabUid = null;
}
},
closeAllCollectionTabs: (state, action) => {
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
state.activeTabUid = null;
}
}
});
export const { addTab, focusTab, updateRequestPaneTabWidth, updateRequestPaneTab, updateResponsePaneTab, closeTabs } =
tabsSlice.actions;
export const {
addTab,
focusTab,
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
closeAllCollectionTabs
} = tabsSlice.actions;
export default tabsSlice.reducer;

View File

@@ -1 +0,0 @@

View File

@@ -1 +1 @@
@import "buttons";
@import 'buttons';

View File

@@ -1,4 +1,3 @@
:root {
--color-brand: #546de5;
--color-text: rgb(52 52 52);
@@ -21,7 +20,8 @@
--color-method-head: rgb(52 52 52);
}
html, body {
html,
body {
margin: 0;
padding: 0;
font-size: 1rem;
@@ -38,15 +38,18 @@ body {
font-size: 0.875rem;
}
body::-webkit-scrollbar, .CodeMirror-vscrollbar::-webkit-scrollbar {
body::-webkit-scrollbar,
.CodeMirror-vscrollbar::-webkit-scrollbar {
width: 0.6rem;
}
body::-webkit-scrollbar-track, .CodeMirror-vscrollbar::-webkit-scrollbar-track {
body::-webkit-scrollbar-track,
.CodeMirror-vscrollbar::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
body::-webkit-scrollbar-thumb, .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
body::-webkit-scrollbar-thumb,
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background-color: #cdcdcd;
border-radius: 5rem;
}

View File

@@ -70,7 +70,7 @@ const darkTheme = {
bg: 'rgb(48, 48, 49)',
hoverBg: '#185387',
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
seperator: '#444',
separator: '#444',
labelBg: '#4a4949'
},
@@ -174,7 +174,7 @@ const darkTheme = {
requestTabs: {
color: '#ccc',
bg: '#2A2D2F',
borromBorder: '#444',
bottomBorder: '#444',
icon: {
color: '#9f9f9f',
hoverColor: 'rgb(204, 204, 204)',

View File

@@ -70,7 +70,7 @@ const lightTheme = {
bg: '#fff',
hoverBg: '#e9e9e9',
shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',
seperator: '#e7e7e7',
separator: '#e7e7e7',
labelBg: '#f3f3f3'
},
@@ -178,7 +178,7 @@ const lightTheme = {
requestTabs: {
color: 'rgb(52, 52, 52)',
bg: '#f7f7f7',
borromBorder: '#efefef',
bottomBorder: '#efefef',
icon: {
color: '#9f9f9f',
hoverColor: 'rgb(76 76 76)',

View File

@@ -8,6 +8,7 @@
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const { get } = require('lodash');
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
@@ -20,7 +21,7 @@ if (!SERVER_RENDERED) {
// str is of format {{variableName}}, extract variableName
// we are seeing that from the gql query editor, the token string is of format variableName
const variableName = str.replace('{{', '').replace('}}', '').trim();
const variableValue = options.variables[variableName];
const variableValue = get(options.variables, variableName);
const into = document.createElement('div');
const descriptionDiv = document.createElement('div');
@@ -65,8 +66,7 @@ if (!SERVER_RENDERED) {
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
return;
}
if (target.className !== 'cm-variable-valid') {
if (!target.classList.contains('cm-variable-valid')) {
return;
}

View File

@@ -54,11 +54,22 @@ const deleteUidsInEnvs = (envs) => {
});
};
const deleteSecretsInEnvs = (envs) => {
each(envs, (env) => {
each(env.variables, (variable) => {
if (variable.secret) {
variable.value = '';
}
});
});
};
const exportCollection = (collection) => {
// delete uids
delete collection.uid;
deleteUidsInItems(collection.items);
deleteUidsInEnvs(collection.environments);
deleteSecretsInEnvs(collection.environments);
transformItem(collection.items);
const fileName = `${collection.name}.json`;

View File

@@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
} else {
collection.items = sortBy(collection.items, (item) => item.seq);
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
@@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
if (targetItemParent) {
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
} else {
collection.items = sortBy(collection.items, (item) => item.seq);
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
@@ -542,6 +546,11 @@ export const getAllVariables = (collection) => {
return {
...environmentVariables,
...collection.collectionVariables
...collection.collectionVariables,
process: {
env: {
...collection.processEnvVariables
}
}
};
};

View File

@@ -1,3 +1,6 @@
import get from 'lodash/get';
import isString from 'lodash/isString';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@@ -5,6 +8,11 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
const pathFoundInVariables = (path, obj) => {
const value = get(obj, path);
return isString(value);
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
let variablesOverlay = {
@@ -15,11 +23,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
while ((ch = stream.next()) != null) {
if (ch == '}' && stream.next() == '}') {
stream.eat('}');
if (word in variables) {
return 'variable-valid';
let found = pathFoundInVariables(word, variables);
if (found) {
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
} else {
return 'variable-invalid';
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
}
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
}
word += ch;
}

View File

@@ -1,4 +1,5 @@
import { customAlphabet } from 'nanoid';
import xmlFormat from 'xml-formatter';
// a customized version of nanoid without using _ and -
export const uuid = () => {
@@ -61,3 +62,34 @@ export const normalizeFileName = (name) => {
return formattedName;
};
export const getContentType = (headers) => {
if (headers && headers.length) {
let contentType = headers
.filter((header) => header[0].toLowerCase() === 'content-type')
.map((header) => {
return header[1];
});
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
return contentType[0];
}
}
return '';
};
export const formatResponse = (response) => {
let type = getContentType(response.headers);
if (type.includes('json')) {
return safeStringifyJSON(response.data, true);
}
if (type.includes('xml')) {
return xmlFormat(response.data, { collapseContent: true });
}
return response.data;
};

View File

@@ -1,6 +1,7 @@
import trim from 'lodash/trim';
import path from 'path';
import slash from './slash';
import platform from 'platform';
export const isElectron = () => {
if (!window) {
@@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
return path.dirname(pathname);
};
export const isWindowsOS = () => {
const os = platform.os;
const osFamily = os.family.toLowerCase();
return osFamily.includes('windows');
};

View File

@@ -30,10 +30,23 @@ const parseGraphQL = (text) => {
}
};
const transformInsomniaRequestItem = (request) => {
const addSuffixToDuplicateName = (item, index, allItems) => {
// Check if the request name already exist and if so add a number suffix
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
if (otherItem.name === item.name && otherIndex < index) {
nameSuffix++;
}
return nameSuffix;
}, 0);
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
}
const transformInsomniaRequestItem = (request, index, allRequests) => {
const name = addSuffixToDuplicateName(request, index, allRequests);
const brunoRequestItem = {
uid: uuid(),
name: request.name,
name,
type: 'http-request',
request: {
url: request.url,
@@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => {
try {
const insomniaExport = JSON.parse(data);
const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find(
(resource) => resource._type === 'workspace' && resource.scope === 'collection'
);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
if (!insomniaCollection) {
reject(new BrunoError('Collection not found inside Insomnia export'));
@@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => {
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
const folders = requestGroups.map((folder) => {
const folders = requestGroups.map((folder, index, allFolder) => {
const name = addSuffixToDuplicateName(folder, index, allFolder);
const requests = resources.filter(
(resource) => resource._type === 'request' && resource.parentId === folder._id
);
return {
uid: uuid(),
name: folder.name,
name,
type: 'folder',
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
};

View File

@@ -91,7 +91,10 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
}
if (bodyMode === 'raw') {
const language = get(i, 'request.body.options.raw.language');
let language = get(i, 'request.body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(i.request.header);
}
if (language === 'json') {
brunoRequestItem.request.body.mode = 'json';
brunoRequestItem.request.body.json = i.request.body.raw;
@@ -131,6 +134,21 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
});
};
const searchLanguageByHeader = (headers) => {
let contentType;
each(headers, (header) => {
if (header.key.toLowerCase() === 'content-type' && !header.disabled) {
if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) {
contentType = 'json';
} else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) {
contentType = 'xml';
}
return false;
}
});
return contentType;
};
const importPostmanV2Collection = (collection) => {
const brunoCollection = {
name: collection.info.name,

View File

@@ -1,9 +1,26 @@
# Changelog
## 0.10.1
- fix(#233) Fixed Issue related to content header parsing
## 0.10.0
- Support for proxying requests through a proxy server
## 0.9.0
- `--output` flag to collect the results of your API tests
## 0.8.0
- `--env-var` flag to set environment variables
- loading environment variables from `.env` file
## 0.7.1
* `--cacert` flag to support custom CA certificates
- `--cacert` flag to support custom CA certificates
## 0.7.0
* `--insecure` flag to disable SSL verification
- `--insecure` flag to disable SSL verification

View File

@@ -0,0 +1,238 @@
{
"summary": {
"totalAssertions": 4,
"passedAssertions": 4,
"failedAssertions": 0,
"totalTests": 0,
"passedTests": 0,
"failedTests": 0
},
"results": [
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v4",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-08gGpUcq2NTnMCVT5AuXxQ0DzGE\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v4",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "mTrKBl5YU6jiAVG-phKT4",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v2",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-lMqxZgVOJiQXjF5yk3AFEU8O9Ro\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v2",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "XsjjGx9cjt5t8tE_t69ZB",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v3",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-tSiYu0/vWz3r+NYRCaed0aW1waw\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v3",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "i_8MmDMtJA9YfvB_FrW15",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "POST",
"url": "http://localhost:8080/test/v1",
"headers": {
"content-type": "application/json"
},
"data": {
"test": "hello"
}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "623",
"etag": "W/\"26f-ku5QGz4p9f02u79vJIve7JH3QYM\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v1",
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
"user-agent": "axios/1.5.0",
"content-length": "16",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "POST",
"body": "{\"test\":\"hello\"}",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {},
"json": {
"test": "hello"
}
}
},
"assertionResults": [
{
"uid": "hNBSF_GBdSTFHNiyCcOn9",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
}
]
}

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/cli",
"version": "0.7.1",
"version": "0.10.1",
"license": "MIT",
"main": "src/index.js",
"bin": {
"bru": "./bin/bru.js"
@@ -20,13 +21,14 @@
"package.json"
],
"dependencies": {
"@usebruno/js": "0.4.0",
"@usebruno/lang": "0.3.0",
"axios": "^1.3.2",
"@usebruno/js": "0.6.0",
"@usebruno/lang": "0.4.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"handlebars": "^4.7.8",
"inquirer": "^9.1.4",
"lodash": "^4.17.21",
"mustache": "^4.2.0",

View File

@@ -5,16 +5,21 @@ With Bruno CLI, you can now run your API collections with ease using simple comm
This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.
## Installation
To install the Bruno CLI, use the node package manager of your choice, such as NPM:
```bash
npm install -g @usebruno/cli
```
## Getting started
Navigate to the directory where your API collection resides, and then run:
```bash
bru run
```
This command will run all the requests in your collection. You can also run a single request by specifying its filename:
```bash
@@ -22,25 +27,37 @@ bru run request.bru
```
Or run all requests in a collection's subfolder:
```bash
bru run folder
```
If you need to use an environment, you can specify it with the --env option:
```bash
bru run folder --env Local
```
If you need to collect the results of your API tests, you can specify the --output option:
```bash
bru run folder --output results.json
```
## Demo
![demo](assets/images/cli-demo.png)
## Support
If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)
Thank you for using Bruno CLI!
## Changelog
See [here](packages/bruno-cli/changelog.md)
## License
[MIT](license.md)
[MIT](license.md)

View File

@@ -1,11 +1,13 @@
const fs = require('fs');
const chalk = require('chalk');
const path = require('path');
const { exists, isFile, isDirectory, getSubDirectories } = require('../utils/filesystem');
const { forOwn } = require('lodash');
const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
const { rpad } = require('../utils/common');
const { bruToJson, getOptions } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -33,7 +35,7 @@ const printRunSummary = (assertionResults, testResults) => {
}
testSummary += `, ${totalAssertions} total`;
console.log("\n" + chalk.bold(assertSummary));
console.log('\n' + chalk.bold(assertSummary));
console.log(chalk.bold(testSummary));
return {
@@ -43,7 +45,7 @@ const printRunSummary = (assertionResults, testResults) => {
totalTests,
passedTests,
failedTests
}
};
};
const getBruFilesRecursively = (dir) => {
@@ -51,33 +53,34 @@ const getBruFilesRecursively = (dir) => {
const getFilesInOrder = (dir) => {
let bruJsons = [];
const traverse = (currentPath) => {
const filesInCurrentDir = fs.readdirSync(currentPath);
if (currentPath.includes('node_modules')) {
return;
}
for (const file of filesInCurrentDir) {
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
// todo: we might need a ignore config inside bruno.json
if (stats.isDirectory() &&
if (
stats.isDirectory() &&
filePath !== environmentsPath &&
!filePath.startsWith(".git") &&
!filePath.startsWith("node_modules")
!filePath.startsWith('.git') &&
!filePath.startsWith('node_modules')
) {
traverse(filePath);
}
}
const currentDirBruJsons = [];
for (const file of filesInCurrentDir) {
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
const bruContent = fs.readFileSync(filePath, 'utf8');
const bruJson = bruToJson(bruContent);
@@ -97,13 +100,12 @@ const getBruFilesRecursively = (dir) => {
bruJsons = bruJsons.concat(currentDirBruJsons);
};
traverse(dir);
return bruJsons;
};
const bruJsons = getFilesInOrder(dir);
return bruJsons;
return getFilesInOrder(dir);
};
const builder = async (yargs) => {
@@ -119,7 +121,16 @@ const builder = async (yargs) => {
})
.option('env', {
describe: 'Environment variables',
type: 'string',
type: 'string'
})
.option('env-var', {
describe: 'Overwrite a single environment variable, multiple usages possible',
type: 'string'
})
.option('output', {
alias: 'o',
describe: 'Path to write JSON results to',
type: 'string'
})
.option('insecure', {
type: 'boolean',
@@ -129,17 +140,19 @@ const builder = async (yargs) => {
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
.example('$0 run folder', 'Run all requests in a folder')
.example('$0 run folder -r', 'Run all requests in a folder recursively')
.example(
'$0 run request.bru --env local --env-var secret=xxx',
'Run a request with the environment set to local and overwrite the variable secret with value xxx'
)
.example(
'$0 run request.bru --output results.json',
'Run a request and write the results to results.json in the current directory'
);
};
const handler = async function (argv) {
try {
let {
filename,
cacert,
env,
insecure,
r: recursive
} = argv;
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
const collectionPath = process.cwd();
// todo
@@ -147,30 +160,33 @@ const handler = async function (argv) {
// will add support in the future to run it from anywhere inside the collection
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
const brunoJsonExists = await exists(brunoJsonPath);
if(!brunoJsonExists) {
if (!brunoJsonExists) {
console.error(chalk.red(`You can run only at the root of a collection`));
return;
}
if(filename && filename.length) {
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
const brunoConfig = JSON.parse(brunoConfigFile);
if (filename && filename.length) {
const pathExists = await exists(filename);
if(!pathExists) {
if (!pathExists) {
console.error(chalk.red(`File or directory ${filename} does not exist`));
return;
}
} else {
filename = "./";
filename = './';
recursive = true;
}
const collectionVariables = {};
let envVars = {};
if(env) {
if (env) {
const envFile = path.join(collectionPath, 'environments', `${env}.bru`);
const envPathExists = await exists(envFile);
if(!envPathExists) {
if (!envPathExists) {
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`));
return;
}
@@ -180,59 +196,90 @@ const handler = async function (argv) {
envVars = getEnvVars(envJson);
}
const options = getOptions();
if(insecure) {
options['insecure'] = true
}
if(cacert && cacert.length) {
if(insecure) {
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
if (envVar) {
let processVars;
if (typeof envVar === 'string') {
processVars = [envVar];
} else if (typeof envVar === 'object' && Array.isArray(envVar)) {
processVars = envVar;
} else {
console.error(chalk.red(`overridable environment variables not parsable: use name=value`));
return;
}
else {
const pathExists = await exists(cacert);
if(pathExists) {
options['cacert'] = cacert
if (processVars && Array.isArray(processVars)) {
for (const value of processVars.values()) {
// split the string at the first equals sign
const match = value.match(/^([^=]+)=(.*)$/);
if (!match) {
console.error(
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
chalk.dim(`${value}`)
);
return;
}
envVars[match[1]] = match[2];
}
else {
}
}
const options = getOptions();
if (insecure) {
options['insecure'] = true;
}
if (cacert && cacert.length) {
if (insecure) {
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
} else {
const pathExists = await exists(cacert);
if (pathExists) {
options['cacert'] = cacert;
} else {
console.error(chalk.red(`Cacert File ${cacert} does not exist`));
}
}
}
// load .env file at root of collection if it exists
const dotEnvPath = path.join(collectionPath, '.env');
const dotEnvExists = await exists(dotEnvPath);
const processEnvVars = {
...process.env
};
if (dotEnvExists) {
const content = fs.readFileSync(dotEnvPath, 'utf8');
const jsonData = dotenvToJson(content);
forOwn(jsonData, (value, key) => {
processEnvVars[key] = value;
});
}
const _isFile = await isFile(filename);
if(_isFile) {
let assertionResults = [];
let testResults = [];
let testrunResults = [];
let bruJsons = [];
if (_isFile) {
console.log(chalk.yellow('Running Request \n'));
const bruContent = fs.readFileSync(filename, 'utf8');
const bruJson = bruToJson(bruContent);
const result = await runSingleRequest(filename, bruJson, collectionPath, collectionVariables, envVars);
if(result) {
const {
assertionResults,
testResults
} = result;
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Done.')));
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
process.exit(1);
}
} else {
process.exit(1);
}
bruJsons.push({
bruFilepath: filename,
bruJson
});
}
const _isDirectory = await isDirectory(filename);
if(_isDirectory) {
let bruJsons = [];
if(!recursive) {
if (_isDirectory) {
if (!recursive) {
console.log(chalk.yellow('Running Folder \n'));
const files = fs.readdirSync(filename);
const bruFiles = files.filter((file) => file.endsWith('.bru'));
for (const bruFile of bruFiles) {
const bruFilepath = path.join(filename, bruFile)
const bruFilepath = path.join(filename, bruFile);
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
const bruJson = bruToJson(bruContent);
bruJsons.push({
@@ -240,8 +287,6 @@ const handler = async function (argv) {
bruJson
});
}
// order requests by sequence
bruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
@@ -250,47 +295,64 @@ const handler = async function (argv) {
} else {
console.log(chalk.yellow('Running Folder Recursively \n'));
bruJsons = await getBruFilesRecursively(filename);
}
let assertionResults = [];
let testResults = [];
for (const iter of bruJsons) {
const {
bruFilepath,
bruJson
} = iter;
const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars);
if(result) {
const {
assertionResults: _assertionResults,
testResults: _testResults
} = result;
assertionResults = assertionResults.concat(_assertionResults);
testResults = testResults.concat(_testResults);
}
}
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Ran all requests.')));
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
process.exit(1);
bruJsons = getBruFilesRecursively(filename);
}
}
for (const iter of bruJsons) {
const { bruFilepath, bruJson } = iter;
const result = await runSingleRequest(
bruFilepath,
bruJson,
collectionPath,
collectionVariables,
envVars,
processEnvVars,
brunoConfig
);
if (result) {
testrunResults.push(result);
const { assertionResults: _assertionResults, testResults: _testResults } = result;
assertionResults = assertionResults.concat(_assertionResults);
testResults = testResults.concat(_testResults);
}
}
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Ran all requests.')));
if (outputPath && outputPath.length) {
const outputDir = path.dirname(outputPath);
const outputDirExists = await exists(outputDir);
if (!outputDirExists) {
console.error(chalk.red(`Output directory ${outputDir} does not exist`));
process.exit(1);
}
const outputJson = {
summary,
results: testrunResults
};
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
}
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
process.exit(1);
}
} catch (err) {
console.log("Something went wrong");
console.log('Something went wrong');
console.error(chalk.red(err.message));
process.exit(1);
}
};
module.exports = {
command,
desc,
builder,
command,
desc,
builder,
handler
};

View File

@@ -5,7 +5,7 @@ const { CLI_EPILOGUE, CLI_VERSION } = require('./constants');
const printBanner = () => {
console.log(chalk.yellow(`Bru CLI ${CLI_VERSION}`));
}
};
const run = async () => {
const argLength = process.argv.length;
@@ -20,7 +20,7 @@ const run = async () => {
.commandDir('commands')
.epilogue(CLI_EPILOGUE)
.usage('Usage: $0 <command> [options]')
.demandCommand(1, "Woof !! Let's play with some apis !!")
.demandCommand(1, "Woof !! Let's play with some APIs !!")
.help('h')
.alias('h', 'help');
};

View File

@@ -1,55 +1,94 @@
const Mustache = require('mustache');
const { each, forOwn } = require('lodash');
const Handlebars = require('handlebars');
const { each, forOwn, cloneDeep } = require('lodash');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
const getContentType = (headers = {}) => {
let contentType = '';
forOwn(headers, (value, key) => {
if (key && key.toLowerCase() === 'content-type') {
contentType = value;
}
});
return contentType;
};
const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
const interpolateEnvVars = (str, processEnvVars) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
return template({
process: {
env: {
...processEnvVars
}
}
});
};
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
// envVars can inturn have values as {{process.env.VAR_NAME}}
// so we need to interpolate envVars first with processEnvVars
forOwn(envVars, (value, key) => {
envVars[key] = interpolateEnvVars(value, processEnvVars);
});
const interpolate = (str) => {
if(!str || !str.length || typeof str !== "string") {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
// collectionVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
};
return Mustache.render(str, combinedVars);
return template(combinedVars);
};
request.url = interpolate(request.url);
forOwn(request.headers, (value, key) => {
request.headers[key] = interpolate(value);
delete request.headers[key];
request.headers[interpolate(key)] = interpolate(value);
});
if(request.headers["content-type"] === "application/json") {
if(typeof request.data === "object") {
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {
}
} catch (err) {}
}
if(typeof request.data === "string") {
if(request.data.length) {
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = interpolate(request.data);
}
}
} else if(request.headers["content-type"] === "application/x-www-form-urlencoded") {
if(typeof request.data === "object") {
} else if (contentType === 'application/x-www-form-urlencoded') {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {
}
} catch (err) {}
}
} else {
request.data = interpolate(request.data);
@@ -59,6 +98,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
param.value = interpolate(param.value);
});
if (request.proxy) {
request.proxy.protocol = interpolate(request.proxy.protocol);
request.proxy.hostname = interpolate(request.proxy.hostname);
request.proxy.port = interpolate(request.proxy.port);
if (request.proxy.auth) {
request.proxy.auth.username = interpolate(request.proxy.auth.username);
request.proxy.auth.password = interpolate(request.proxy.auth.password);
}
}
return request;
};

View File

@@ -11,7 +11,15 @@ const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@use
const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru');
const runSingleRequest = async function (filename, bruJson, collectionPath, collectionVariables, envVariables) {
const runSingleRequest = async function (
filename,
bruJson,
collectionPath,
collectionVariables,
envVariables,
processEnvVars,
brunoConfig
) {
let request;
try {
@@ -19,7 +27,7 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
@@ -30,48 +38,87 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
// run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req');
if(preRequestVars && preRequestVars.length) {
if (preRequestVars && preRequestVars.length) {
const varsRuntime = new VarsRuntime();
varsRuntime.runPreRequestVars(preRequestVars, request, envVariables, collectionVariables, collectionPath);
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVariables,
collectionVariables,
collectionPath,
processEnvVars
);
}
// run pre request script
const requestScriptFile = get(bruJson, 'request.script.req');
if(requestScriptFile && requestScriptFile.length) {
if (requestScriptFile && requestScriptFile.length) {
const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath);
await scriptRuntime.runRequestScript(
requestScriptFile,
request,
envVariables,
collectionVariables,
collectionPath,
null,
processEnvVars
);
}
// set proxy if enabled
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
const proxyHostname = get(brunoConfig, 'proxy.hostname');
const proxyPort = get(brunoConfig, 'proxy.port');
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const proxyConfig = {
protocol: proxyProtocol,
hostname: proxyHostname,
port: proxyPort
};
if (proxyAuthEnabled) {
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
proxyConfig.auth = {
username: proxyAuthUsername,
password: proxyAuthPassword
};
}
request.proxy = proxyConfig;
}
// interpolate variables inside request
interpolateVars(request, envVariables, collectionVariables);
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
const options = getOptions();
const insecure = get(options, 'insecure', false);
const httpsAgentRequestFields = {};
if(insecure) {
if (insecure) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
else {
} else {
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
const cacert = cacertArray.find(el => el);
if(cacert && cacert.length > 1) {
const cacert = cacertArray.find((el) => el);
if (cacert && cacert.length > 1) {
try {
caCrt = fs.readFileSync(cacert);
httpsAgentRequestFields['ca'] = caCrt;
} catch(err) {
} catch (err) {
console.log('Error reading CA cert file:' + cacert, err);
}
}
}
if(Object.keys(httpsAgentRequestFields).length > 0) {
if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
// stringify the request url encoded params
if(request.headers['content-type'] === 'application/x-www-form-urlencoded') {
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
request.data = qs.stringify(request.data);
}
@@ -82,27 +129,51 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
// run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res');
if(postResponseVars && postResponseVars.length) {
if (postResponseVars && postResponseVars.length) {
const varsRuntime = new VarsRuntime();
varsRuntime.runPostResponseVars(postResponseVars, request, response, envVariables, collectionVariables, collectionPath);
varsRuntime.runPostResponseVars(
postResponseVars,
request,
response,
envVariables,
collectionVariables,
collectionPath,
processEnvVars
);
}
// run post response script
const responseScriptFile = get(bruJson, 'request.script.res');
if(responseScriptFile && responseScriptFile.length) {
if (responseScriptFile && responseScriptFile.length) {
const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath);
await scriptRuntime.runResponseScript(
responseScriptFile,
request,
response,
envVariables,
collectionVariables,
collectionPath,
null,
processEnvVars
);
}
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
if(assertions && assertions.length) {
if (assertions && assertions.length) {
const assertRuntime = new AssertRuntime();
assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath);
assertionResults = assertRuntime.runAssertions(
assertions,
request,
response,
envVariables,
collectionVariables,
collectionPath
);
each(assertionResults, (r) => {
if(r.status === 'pass') {
if (r.status === 'pass') {
console.log(chalk.green(``) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
} else {
console.log(chalk.red(``) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
@@ -114,15 +185,24 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
// run tests
let testResults = [];
const testFile = get(bruJson, 'request.tests');
if(testFile && testFile.length) {
if (testFile && testFile.length) {
const testRuntime = new TestRuntime();
const result = testRuntime.runTests(testFile, request, response, envVariables, collectionVariables, collectionPath);
const result = await testRuntime.runTests(
testFile,
request,
response,
envVariables,
collectionVariables,
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}
if(testResults && testResults.length) {
if (testResults && testResults.length) {
each(testResults, (testResult) => {
if(testResult.status === 'pass') {
if (testResult.status === 'pass') {
console.log(chalk.green(``) + chalk.dim(testResult.description));
} else {
console.log(chalk.red(``) + chalk.red(testResult.description));
@@ -131,36 +211,74 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
}
return {
request: {
method: request.method,
url: request.url,
headers: request.headers,
data: request.data
},
response: {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
},
assertionResults,
testResults
};
} catch (err) {
if(err && err.response) {
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`));
if (err && err.response) {
console.log(
chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`)
);
// run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res');
if(postResponseVars && postResponseVars.length) {
if (postResponseVars && postResponseVars.length) {
const varsRuntime = new VarsRuntime();
varsRuntime.runPostResponseVars(postResponseVars, request, err.response, envVariables, collectionVariables, collectionPath);
varsRuntime.runPostResponseVars(
postResponseVars,
request,
err.response,
envVariables,
collectionVariables,
collectionPath,
processEnvVars
);
}
// run post response script
const responseScriptFile = get(bruJson, 'request.script.res');
if(responseScriptFile && responseScriptFile.length) {
if (responseScriptFile && responseScriptFile.length) {
const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runResponseScript(responseScriptFile, request, err.response, envVariables, collectionVariables, collectionPath);
await scriptRuntime.runResponseScript(
responseScriptFile,
request,
err.response,
envVariables,
collectionVariables,
collectionPath,
null,
processEnvVars
);
}
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
if(assertions && assertions.length) {
if (assertions && assertions.length) {
const assertRuntime = new AssertRuntime();
assertionResults = assertRuntime.runAssertions(assertions, request, err.response, envVariables, collectionVariables, collectionPath);
assertionResults = assertRuntime.runAssertions(
assertions,
request,
err.response,
envVariables,
collectionVariables,
collectionPath
);
each(assertionResults, (r) => {
if(r.status === 'pass') {
if (r.status === 'pass') {
console.log(chalk.green(``) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
} else {
console.log(chalk.red(``) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
@@ -172,15 +290,24 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
// run tests
let testResults = [];
const testFile = get(bruJson, 'request.tests');
if(testFile && testFile.length) {
if (testFile && testFile.length) {
const testRuntime = new TestRuntime();
const result = testRuntime.runTests(testFile, request, err.response, envVariables, collectionVariables, collectionPath);
const result = await testRuntime.runTests(
testFile,
request,
err.response,
envVariables,
collectionVariables,
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}
if(testResults && testResults.length) {
if (testResults && testResults.length) {
each(testResults, (testResult) => {
if(testResult.status === 'pass') {
if (testResult.status === 'pass') {
console.log(chalk.green(``) + chalk.dim(testResult.description));
} else {
console.log(chalk.red(``) + chalk.red(testResult.description));

View File

@@ -9,10 +9,10 @@ Mustache.escape = function (value) {
/**
* The transformer function for converting a BRU file to JSON.
*
*
* We map the json response from the bru lang and transform it into the DSL
* format that is used by the bruno app
*
*
* @param {string} bru The BRU file content.
* @returns {object} The JSON representation of the BRU file.
*/
@@ -20,35 +20,35 @@ const bruToJson = (bru) => {
try {
const json = bruToJsonV2(bru);
let requestType = _.get(json, "meta.type");
if(requestType === "http") {
requestType = "http-request"
} else if(requestType === "graphql") {
requestType = "graphql-request";
let requestType = _.get(json, 'meta.type');
if (requestType === 'http') {
requestType = 'http-request';
} else if (requestType === 'graphql') {
requestType = 'graphql-request';
} else {
requestType = "http";
requestType = 'http';
}
const sequence = _.get(json, "meta.seq")
const sequence = _.get(json, 'meta.seq');
const transformedJson = {
"type": requestType,
"name": _.get(json, "meta.name"),
"seq": !isNaN(sequence) ? Number(sequence) : 1,
"request": {
"method": _.upperCase(_.get(json, "http.method")),
"url": _.get(json, "http.url"),
"params": _.get(json, "query", []),
"headers": _.get(json, "headers", []),
"body": _.get(json, "body", {}),
"vars": _.get(json, "vars", []),
"assertions": _.get(json, "assertions", []),
"script": _.get(json, "script", ""),
"tests": _.get(json, "tests", "")
type: requestType,
name: _.get(json, 'meta.name'),
seq: !isNaN(sequence) ? Number(sequence) : 1,
request: {
method: _.upperCase(_.get(json, 'http.method')),
url: _.get(json, 'http.url'),
params: _.get(json, 'query', []),
headers: _.get(json, 'headers', []),
body: _.get(json, 'body', {}),
vars: _.get(json, 'vars', []),
assertions: _.get(json, 'assertions', []),
script: _.get(json, 'script', ''),
tests: _.get(json, 'tests', '')
}
};
transformedJson.request.body.mode = _.get(json, "http.body", "none");
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
return transformedJson;
} catch (err) {
@@ -72,7 +72,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
_.each(variables, (variable) => {
if(variable.enabled) {
if (variable.enabled) {
envVars[variable.name] = Mustache.escape(variable.value);
}
});
@@ -83,7 +83,7 @@ const getEnvVars = (environment = {}) => {
const options = {};
const getOptions = () => {
return options;
}
};
module.exports = {
bruToJson,

View File

@@ -12,7 +12,7 @@ const rpad = (str, width) => {
paddedStr = paddedStr + ' ';
}
return paddedStr;
}
};
module.exports = {
lpad,

View File

@@ -2,7 +2,7 @@ const path = require('path');
const fs = require('fs-extra');
const fsPromises = require('fs/promises');
const exists = async p => {
const exists = async (p) => {
try {
await fsPromises.access(p);
return true;
@@ -11,7 +11,7 @@ const exists = async p => {
}
};
const isSymbolicLink = filepath => {
const isSymbolicLink = (filepath) => {
try {
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();
} catch (_) {
@@ -19,7 +19,7 @@ const isSymbolicLink = filepath => {
}
};
const isFile = filepath => {
const isFile = (filepath) => {
try {
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
} catch (_) {
@@ -27,7 +27,7 @@ const isFile = filepath => {
}
};
const isDirectory = dirPath => {
const isDirectory = (dirPath) => {
try {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
} catch (_) {
@@ -35,14 +35,14 @@ const isDirectory = dirPath => {
}
};
const normalizeAndResolvePath = pathname => {
const normalizeAndResolvePath = (pathname) => {
if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname);
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
if (isFile(targetPath) || isDirectory(targetPath)) {
return path.resolve(targetPath);
}
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`);
return '';
}
return path.resolve(pathname);
@@ -51,29 +51,29 @@ const normalizeAndResolvePath = pathname => {
const writeFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content, {
encoding: "utf8"
encoding: 'utf8'
});
} catch (err) {
return Promise.reject(err);
}
};
const hasJsonExtension = filename => {
if (!filename || typeof filename !== 'string') return false
return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const hasJsonExtension = (filename) => {
if (!filename || typeof filename !== 'string') return false;
return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
};
const hasBruExtension = filename => {
if (!filename || typeof filename !== 'string') return false
return ['bru'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const hasBruExtension = (filename) => {
if (!filename || typeof filename !== 'string') return false;
return ['bru'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
};
const createDirectory = async (dir) => {
if(!dir) {
if (!dir) {
throw new Error(`directory: path is null`);
}
if (fs.existsSync(dir)){
if (fs.existsSync(dir)) {
throw new Error(`directory: ${dir} already exists`);
}
@@ -93,15 +93,15 @@ const searchForFiles = (dir, extension) => {
}
}
return results;
}
};
const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
const stripExtension = (filename = '') => {
return filename.replace(/\.[^/.]+$/, "");
}
return filename.replace(/\.[^/.]+$/, '');
};
const getSubDirectories = (dir) => {
try {
@@ -112,7 +112,7 @@ const getSubDirectories = (dir) => {
})
.sort();
return subDirectories;
return subDirectories;
} catch (err) {
return [];
}

View File

@@ -1,7 +1,7 @@
{
"version": "v0.14.0",
"version": "v0.16.6",
"name": "bruno",
"description": "Opensource API Client",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
"private": true,
"main": "src/index.js",
@@ -10,13 +10,15 @@
"clean": "rimraf dist",
"dev": "electron .",
"dist": "electron-builder --mac --config electron-builder-config.js",
"pack": "electron-builder --dir"
"pack": "electron-builder --dir",
"test": "jest"
},
"dependencies": {
"@usebruno/js": "0.4.0",
"@usebruno/lang": "0.3.0",
"@usebruno/schema": "0.3.1",
"axios": "^0.26.0",
"@usebruno/js": "0.6.0",
"@usebruno/lang": "0.4.0",
"@usebruno/schema": "0.5.0",
"about-window": "^1.15.2",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
"dotenv": "^16.0.3",
@@ -27,10 +29,12 @@
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",
"handlebars": "^4.7.8",
"is-valid-path": "^0.1.1",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0",
"uuid": "^9.0.0",
"vm2": "^3.9.13",

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,8 @@
.versions {
-webkit-user-select: text;
user-select: text;
}
.title {
-webkit-user-select: text;
user-select: text;
}

View File

@@ -5,39 +5,33 @@ const Yup = require('yup');
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
// uid inside collections is deprecated, but we still need to validate it
// for backward compatibility
const uidSchema = Yup.string()
.length(21, 'uid must be 21 characters in length')
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric');
// todo: bruno.json config schema validation errors must be propagated to the UI
const configSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less'),
name: Yup.string().max(256, 'name must be 256 characters or less').required('name is required'),
type: Yup.string().oneOf(['collection']).required('type is required'),
version: Yup.string().oneOf(['1']).required('type is required')
}).noUnknown(true).strict();
});
const readConfigFile = async (pathname) => {
try {
const jsonData = fs.readFileSync(pathname, 'utf8');
return JSON.parse(jsonData);
} catch(err) {
return Promise.reject(new Error("Unable to parse json in bruno.json"));
} catch (err) {
return Promise.reject(new Error('Unable to parse json in bruno.json'));
}
}
};
const validateSchema = async (config) => {
try {
await configSchema.validate(config);
} catch(err) {
return Promise.reject(new Error("bruno.json format is invalid"));
} catch (err) {
return Promise.reject(new Error('bruno.json format is invalid'));
}
};
const getCollectionConfigFile = async (pathname) => {
const configFilePath = path.join(pathname, 'bruno.json');
if (!fs.existsSync(configFilePath)){
if (!fs.existsSync(configFilePath)) {
throw new Error(`The collection is not valid (bruno.json not found)`);
}
@@ -45,7 +39,7 @@ const getCollectionConfigFile = async (pathname) => {
await validateSchema(config);
return config;
}
};
const openCollectionDialog = async (win, watcher) => {
const { filePaths } = await dialog.showOpenDialog(win, {
@@ -60,22 +54,20 @@ const openCollectionDialog = async (win, watcher) => {
console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`);
}
}
}
};
const openCollection = async (win, watcher, collectionPath, options = {}) => {
if(!watcher.hasWatcher(collectionPath)) {
if (!watcher.hasWatcher(collectionPath)) {
try {
const {
name
} = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
const brunoConfig = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
win.webContents.send('main:collection-opened', collectionPath, uid, name);
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', win, collectionPath, uid);
} catch(err) {
if(!options.dontSendDisplayErrors) {
} catch (err) {
if (!options.dontSendDisplayErrors) {
win.webContents.send('main:display-error', {
error: err.message || 'An error occured while opening the local collection'
error: err.message || 'An error occurred while opening the local collection'
});
}
}

View File

@@ -1,12 +1,14 @@
const { ipcMain } = require('electron');
const openAboutWindow = require('about-window').default;
const { join } = require('path');
const template = [
{
label: 'Collection',
submenu: [
{
label: 'Open Local Collection',
click () {
label: 'Open Collection',
click() {
ipcMain.emit('main:open-collection');
}
},
@@ -14,39 +16,48 @@ const template = [
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo'},
{ role: 'redo'},
{ type: 'separator'},
{ role: 'cut'},
{ role: 'copy'},
{ role: 'paste'}
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' }
]
},
{
label: 'View',
submenu: [
{ role: 'toggledevtools'},
{ type: 'separator'},
{ role: 'resetzoom'},
{ role: 'zoomin'},
{ role: 'zoomout'},
{ type: 'separator'},
{ role: 'togglefullscreen'}
label: 'View',
submenu: [
{ role: 'toggledevtools' },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
role: 'window',
submenu: [
{ role: 'minimize'},
{ role: 'close'}
]
role: 'window',
submenu: [{ role: 'minimize' }, { role: 'close' }]
},
{
role: 'help',
submenu: [
{ label: 'Learn More'}
{
label: 'About Bruno',
click: () =>
openAboutWindow({
product_name: 'Bruno',
icon_path: join(__dirname, '../about/256x256.png'),
css_path: join(__dirname, '../about/about.css'),
homepage: 'https://www.usebruno.com/',
package_json_dir: join(__dirname, '../..')
})
},
{ label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }
]
}
];

View File

@@ -3,22 +3,19 @@ const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
const {
bruToEnvJson,
envJsonToBru,
bruToJson,
jsonToBru
} = require('../bru');
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const {
isLegacyEnvFile,
migrateLegacyEnvFile,
isLegacyBruFile,
migrateLegacyBruFile
} = require('../bru/migrate');
const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
const { itemSchema } = require('@usebruno/schema');
const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
@@ -27,6 +24,20 @@ const isJsonEnvironmentConfig = (pathname, collectionPath) => {
return dirname === collectionPath && basename === 'environments.json';
};
const isDotEnvFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === '.env';
};
const isBrunoConfigFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === 'bruno.json';
};
const isBruEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
@@ -46,18 +57,24 @@ const hydrateRequestWithUuid = (request, pathname) => {
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
params.forEach((param) => param.uid = uuid());
headers.forEach((header) => header.uid = uuid());
requestVars.forEach((variable) => variable.uid = uuid());
responseVars.forEach((variable) => variable.uid = uuid());
assertions.forEach((assertion) => assertion.uid = uuid());
bodyFormUrlEncoded.forEach((param) => param.uid = uuid());
bodyMultipartForm.forEach((param) => param.uid = uuid());
params.forEach((param) => (param.uid = uuid()));
headers.forEach((header) => (header.uid = uuid()));
requestVars.forEach((variable) => (variable.uid = uuid()));
responseVars.forEach((variable) => (variable.uid = uuid()));
assertions.forEach((assertion) => (assertion.uid = uuid()));
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
return request;
}
};
const addEnvironmentFile = async (win, pathname, collectionUid) => {
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
return secrets && secrets.length > 0;
};
const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
try {
const basename = path.basename(pathname);
const file = {
@@ -65,13 +82,13 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
collectionUid,
pathname,
name: basename
},
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
// migrate old env json to bru file
if(isLegacyEnvFile(bruContent)) {
if (isLegacyEnvFile(bruContent)) {
bruContent = await migrateLegacyEnvFile(bruContent, pathname);
}
@@ -79,14 +96,26 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
// hydrate environment variables with secrets
if (envHasSecrets(file.data)) {
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
_.each(envSecrets, (secret) => {
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
if (variable) {
variable.value = decryptString(secret.value);
}
});
}
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
} catch (err) {
console.error(err)
console.error(err);
}
};
const changeEnvironmentFile = async (win, pathname, collectionUid) => {
const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
try {
const basename = path.basename(pathname);
const file = {
@@ -101,14 +130,25 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => {
file.data = bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
// hydrate environment variables with secrets
if (envHasSecrets(file.data)) {
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
_.each(envSecrets, (secret) => {
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
if (variable) {
variable.value = decryptString(secret.value);
}
});
}
// we are reusing the addEnvironmentFile event itself
// this is because the uid of the pathname remains the same
// and the collection tree will be able to update the existing environment
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
} catch (err) {
console.error(err)
console.error(err);
}
};
@@ -118,24 +158,54 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
name: path.basename(pathname)
},
data: {
uid: getRequestUid(pathname),
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4)
}
};
win.webContents.send('main:collection-tree-updated', 'unlinkEnvironmentFile', file);
} catch (err) {
console.error(err)
console.error(err);
}
};
const add = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher add: ${pathname}`);
if(isJsonEnvironmentConfig(pathname, collectionPath)) {
if (isBrunoConfigFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const brunoConfig = JSON.parse(content);
setBrunoConfig(collectionUid, brunoConfig);
} catch (err) {
console.error(err);
}
}
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isJsonEnvironmentConfig(pathname, collectionPath)) {
try {
const dirname = path.dirname(pathname);
const bruContent = fs.readFileSync(pathname, 'utf8');
@@ -147,7 +217,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
fs.mkdirSync(envDirectory);
}
for(const env of jsonData) {
for (const env of jsonData) {
const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
const bruContent = envJsonToBru(env);
await writeFile(bruEnvFilename, bruContent);
@@ -161,12 +231,12 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
return;
}
if(isBruEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid);
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
// migrate old json files to bru
if(hasJsonExtension(pathname)) {
if (hasJsonExtension(pathname)) {
try {
const json = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(json);
@@ -178,7 +248,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
const re = /(.*)\.json$/;
const subst = `$1.bru`;
const bruFilename = pathname.replace(re, subst);
await writeFile(bruFilename, content);
await fs.unlinkSync(pathname);
} catch (err) {
@@ -186,20 +256,20 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
}
if(hasBruExtension(pathname)) {
if (hasBruExtension(pathname)) {
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
name: path.basename(pathname)
}
}
};
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
// migrate old bru format to new bru format
if(isLegacyBruFile(bruContent)) {
if (isLegacyBruFile(bruContent)) {
bruContent = await migrateLegacyBruFile(bruContent, pathname);
}
@@ -207,7 +277,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (err) {
console.error(err)
console.error(err);
}
}
};
@@ -215,7 +285,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
const addDirectory = (win, pathname, collectionUid, collectionPath) => {
const envDirectory = path.join(collectionPath, 'environments');
if(pathname === envDirectory) {
if (pathname === envDirectory) {
return;
}
@@ -223,44 +293,79 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
name: path.basename(pathname)
}
};
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
};
const change = async (win, pathname, collectionUid, collectionPath) => {
if(isBruEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid);
if (isBrunoConfigFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const brunoConfig = JSON.parse(content);
const payload = {
collectionUid,
brunoConfig: brunoConfig
};
setBrunoConfig(collectionUid, brunoConfig);
win.webContents.send('main:bruno-config-update', payload);
} catch (err) {
console.error(err);
}
}
if(hasBruExtension(pathname)) {
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
if (hasBruExtension(pathname)) {
try {
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
name: path.basename(pathname)
}
};
const bru = fs.readFileSync(pathname, 'utf8');
file.data = bruToJson(bru);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
} catch (err) {
console.error(err)
console.error(err);
}
}
};
const unlink = (win, pathname, collectionUid, collectionPath) => {
if(isBruEnvironmentConfig(pathname, collectionPath)) {
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return unlinkEnvironmentFile(win, pathname, collectionUid);
}
if(hasBruExtension(pathname)) {
if (hasBruExtension(pathname)) {
const file = {
meta: {
collectionUid,
@@ -270,12 +375,12 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
};
win.webContents.send('main:collection-tree-updated', 'unlink', file);
}
}
};
const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
const envDirectory = path.join(collectionPath, 'environments');
if(pathname === envDirectory) {
if (pathname === envDirectory) {
return;
}
@@ -287,15 +392,15 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
}
};
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
}
};
class Watcher {
constructor () {
constructor() {
this.watchers = {};
}
addWatcher (win, watchPath, collectionUid) {
if(this.watchers[watchPath]) {
addWatcher(win, watchPath, collectionUid) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
@@ -309,7 +414,7 @@ class Watcher {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
usePolling: false,
ignored: path => ["node_modules", ".git", "bruno.json"].some(s => path.includes(s)),
ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
persistent: true,
ignorePermissionErrors: true,
awaitWriteFinish: {
@@ -318,28 +423,28 @@ class Watcher {
},
depth: 20
});
watcher
.on('add', pathname => add(win, pathname, collectionUid, watchPath))
.on('addDir', pathname => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', pathname => change(win, pathname, collectionUid, watchPath))
.on('unlink', pathname => unlink(win, pathname, collectionUid, watchPath))
.on('unlinkDir', pathname => unlinkDir(win, pathname, collectionUid, watchPath))
self.watchers[watchPath] = watcher;
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath));
self.watchers[watchPath] = watcher;
}, 100);
}
hasWatcher (watchPath) {
hasWatcher(watchPath) {
return this.watchers[watchPath];
}
removeWatcher (watchPath, win) {
if(this.watchers[watchPath]) {
removeWatcher(watchPath, win) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
this.watchers[watchPath] = null;
}
}
};
}
module.exports = Watcher;
module.exports = Watcher;

View File

@@ -1,10 +1,5 @@
const _ = require('lodash');
const {
bruToJsonV2,
jsonToBruV2,
bruToEnvJsonV2,
envJsonToBruV2
} = require('@usebruno/lang');
const { bruToJsonV2, jsonToBruV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
const { each } = require('lodash');
const bruToEnvJson = (bru) => {
@@ -12,33 +7,33 @@ const bruToEnvJson = (bru) => {
const json = bruToEnvJsonV2(bru);
// the app env format requires each variable to have a type
// this need to be evaulated and safely removed
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if(json && json.variables && json.variables.length) {
each(json.variables, (v) => v.type = "text");
if (json && json.variables && json.variables.length) {
each(json.variables, (v) => (v.type = 'text'));
}
return json;
} catch (error) {
return Promise.reject(e);
return Promise.reject(error);
}
}
};
const envJsonToBru = (json) => {
try {
const bru = envJsonToBruV2(json);
return bru;
} catch (error) {
return Promise.reject(e);
return Promise.reject(error);
}
}
};
/**
* The transformer function for converting a BRU file to JSON.
*
*
* We map the json response from the bru lang and transform it into the DSL
* format that the app users
*
* format that the app uses
*
* @param {string} bru The BRU file content.
* @returns {object} The JSON representation of the BRU file.
*/
@@ -46,35 +41,35 @@ const bruToJson = (bru) => {
try {
const json = bruToJsonV2(bru);
let requestType = _.get(json, "meta.type");
if(requestType === "http") {
requestType = "http-request"
} else if(requestType === "graphql") {
requestType = "graphql-request";
let requestType = _.get(json, 'meta.type');
if (requestType === 'http') {
requestType = 'http-request';
} else if (requestType === 'graphql') {
requestType = 'graphql-request';
} else {
requestType = "http-request";
requestType = 'http-request';
}
const sequence = _.get(json, "meta.seq")
const sequence = _.get(json, 'meta.seq');
const transformedJson = {
"type": requestType,
"name": _.get(json, "meta.name"),
"seq": !isNaN(sequence) ? Number(sequence) : 1,
"request": {
"method": _.upperCase(_.get(json, "http.method")),
"url": _.get(json, "http.url"),
"params": _.get(json, "query", []),
"headers": _.get(json, "headers", []),
"body": _.get(json, "body", {}),
"script": _.get(json, "script", {}),
"vars": _.get(json, "vars", {}),
"assertions": _.get(json, "assertions", []),
"tests": _.get(json, "tests", "")
type: requestType,
name: _.get(json, 'meta.name'),
seq: !isNaN(sequence) ? Number(sequence) : 1,
request: {
method: _.upperCase(_.get(json, 'http.method')),
url: _.get(json, 'http.url'),
params: _.get(json, 'query', []),
headers: _.get(json, 'headers', []),
body: _.get(json, 'body', {}),
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
assertions: _.get(json, 'assertions', []),
tests: _.get(json, 'tests', '')
}
};
transformedJson.request.body.mode = _.get(json, "http.body", "none");
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
return transformedJson;
} catch (e) {
@@ -83,28 +78,28 @@ const bruToJson = (bru) => {
};
/**
* The transformer function for converting a JSON to BRU file.
*
*
* We map the json response from the app and transform it into the DSL
* format that the bru lang understands
*
*
* @param {object} json The JSON representation of the BRU file.
* @returns {string} The BRU file content.
*/
const jsonToBru = (json) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = "http";
type = 'http';
} else if (type === 'graphql-request') {
type = "graphql";
type = 'graphql';
} else {
type = "http";
type = 'http';
}
const bruJson = {
meta: {
name: _.get(json, 'name'),
type: type,
seq: _.get(json, 'seq'),
seq: _.get(json, 'seq')
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
@@ -120,7 +115,7 @@ const jsonToBru = (json) => {
res: _.get(json, 'request.vars.res', [])
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
tests: _.get(json, 'request.tests', '')
};
return jsonToBruV2(bruJson);
@@ -130,5 +125,5 @@ module.exports = {
bruToJson,
jsonToBru,
bruToEnvJson,
envJsonToBru,
envJsonToBru
};

View File

@@ -34,15 +34,15 @@ const isLegacyBruFile = (bruContent = '') => {
for (let line of lines) {
line = line.trim();
if (line.startsWith("name")) {
if (line.startsWith('name')) {
hasName = true;
} else if (line.startsWith("method")) {
} else if (line.startsWith('method')) {
hasMethod = true;
} else if (line.startsWith("url")) {
} else if (line.startsWith('url')) {
hasUrl = true;
}
}
return hasName && hasMethod && hasUrl;
};
@@ -51,16 +51,16 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = "http";
type = 'http';
} else if (type === 'graphql-request') {
type = "graphql";
type = 'graphql';
} else {
type = "http";
type = 'http';
}
let script = {};
let legacyScript = _.get(json, 'request.script');
if(legacyScript && legacyScript.trim().length > 0) {
if (legacyScript && legacyScript.trim().length > 0) {
script = {
res: legacyScript
};
@@ -70,7 +70,7 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
meta: {
name: _.get(json, 'name'),
type: type,
seq: _.get(json, 'seq'),
seq: _.get(json, 'seq')
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
@@ -81,7 +81,7 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
headers: _.get(json, 'request.headers', []),
body: _.get(json, 'request.body', {}),
script: script,
tests: _.get(json, 'request.tests', ''),
tests: _.get(json, 'request.tests', '')
};
const newBruContent = jsonToBruV2(bruJson);
@@ -89,11 +89,11 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
await writeFile(pathname, newBruContent);
return newBruContent;
}
};
module.exports = {
isLegacyEnvFile,
migrateLegacyEnvFile,
isLegacyBruFile,
migrateLegacyBruFile
};
};

View File

@@ -2,7 +2,7 @@
* we maintain a cache of request uids to ensure that we
* preserve the same uid for a request even when the request
* moves to a different location
*
*
* In the past, we used to generate unique ids based on the
* pathname of the request, but we faced problems when implementing
* functionality where the user can move the request to a different

View File

@@ -5,7 +5,7 @@ const { BrowserWindow, app, Menu } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
const menuTemplate = require('./app/menu-template');
const LastOpenedCollections = require('./app/last-opened-collections');
const LastOpenedCollections = require('./store/last-opened-collections');
const registerNetworkIpc = require('./ipc/network');
const registerCollectionsIpc = require('./ipc/collection');
const Watcher = require('./app/watcher');
@@ -18,7 +18,7 @@ setContentSecurityPolicy(`
connect-src * 'unsafe-inline';
base-uri 'none';
form-action 'none';
img-src 'self' data:image/svg+xml
img-src 'self' data:image/svg+xml;
`);
const menu = Menu.buildFromTemplate(menuTemplate);
@@ -35,8 +35,8 @@ app.on('ready', async () => {
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, "preload.js")
},
preload: path.join(__dirname, 'preload.js')
}
});
const url = isDev
@@ -50,7 +50,7 @@ app.on('ready', async () => {
mainWindow.loadURL(url);
watcher = new Watcher();
mainWindow.webContents.on('new-window', function(e, url) {
mainWindow.webContents.on('new-window', function (e, url) {
e.preventDefault();
require('electron').shell.openExternal(url);
});

View File

@@ -1,12 +1,8 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain } = require('electron');
const {
envJsonToBru,
bruToJson,
jsonToBru
} = require('../bru');
const { ipcMain, shell } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const {
isValidPathname,
@@ -15,13 +11,23 @@ const {
isDirectory,
browseDirectory,
createDirectory,
searchForBruFiles
searchForBruFiles,
sanitizeDirectoryName
} = require('../utils/filesystem');
const { stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require("../app/preferences");
const { setPreferences } = require('../store/preferences');
const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
return secrets && secrets.length > 0;
};
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
// browse directory
@@ -36,35 +42,39 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// create collection
ipcMain.handle('renderer:create-collection', async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)){
throw new Error(`collection: ${dirPath} already exists`);
ipcMain.handle(
'renderer:create-collection',
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
if (!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
}
await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath);
const brunoConfig = {
version: '1',
name: collectionName,
type: 'collection'
};
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(dirPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
return;
} catch (error) {
return Promise.reject(error);
}
if(!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
}
await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath);
const content = await stringifyJson({
version: '1',
name: collectionName,
type: 'collection'
});
await writeFile(path.join(dirPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', dirPath, uid, collectionName);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
return;
} catch (error) {
return Promise.reject(error);
}
});
);
// rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
@@ -94,7 +104,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
if (fs.existsSync(pathname)){
if (fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} already exists`);
}
@@ -108,7 +118,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// save request
ipcMain.handle('renderer:save-request', async (event, pathname, request) => {
try {
if (!fs.existsSync(pathname)){
if (!fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} does not exist`);
}
@@ -123,12 +133,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
if (!fs.existsSync(envDirPath)){
if (!fs.existsSync(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${name}.bru`);
if (fs.existsSync(envFilePath)){
if (fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} already exists`);
}
@@ -145,15 +155,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
if (!fs.existsSync(envDirPath)){
if (!fs.existsSync(envDirPath)) {
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
if (!fs.existsSync(envFilePath)){
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
if (envHasSecrets(environment)) {
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
}
const content = envJsonToBru(environment);
await writeFile(envFilePath, content);
} catch (error) {
@@ -166,16 +180,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)){
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
if (fs.existsSync(newEnvFilePath)){
if (fs.existsSync(newEnvFilePath)) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
fs.renameSync(envFilePath, newEnvFilePath);
environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName);
} catch (error) {
return Promise.reject(error);
}
@@ -186,11 +202,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)){
if (!fs.existsSync(envFilePath)) {
throw new Error(`environment: ${envFilePath} does not exist`);
}
fs.unlinkSync(envFilePath);
environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName);
} catch (error) {
return Promise.reject(error);
}
@@ -199,18 +217,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
if (!fs.existsSync(oldPath)){
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
if (fs.existsSync(newPath)){
if (fs.existsSync(newPath)) {
throw new Error(`path: ${oldPath} already exists`);
}
// if its directory, rename and return
if(isDirectory(oldPath)) {
if (isDirectory(oldPath)) {
const bruFilesAtSource = await searchForBruFiles(oldPath);
for(let bruFile of bruFilesAtSource) {
for (let bruFile of bruFilesAtSource) {
const newBruFilePath = bruFile.replace(oldPath, newPath);
moveRequestUid(bruFile, newBruFilePath);
}
@@ -218,7 +236,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
const isBru = hasBruExtension(oldPath);
if(!isBru) {
if (!isBru) {
throw new Error(`path: ${oldPath} is not a bru file`);
}
@@ -241,8 +259,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// new folder
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
try {
if (!fs.existsSync(pathname)){
fs.mkdirSync(pathname);
if (!fs.existsSync(pathname)) {
fs.mkdirSync(pathname);
} else {
return Promise.reject(new Error('The directory already exists'));
}
@@ -254,20 +272,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// delete file/folder
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
if(type === 'folder') {
if(!fs.existsSync(pathname)) {
if (type === 'folder') {
if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The directory does not exist'));
}
// delete the request uid mappings
const bruFilesAtSource = await searchForBruFiles(pathname);
for(let bruFile of bruFilesAtSource) {
for (let bruFile of bruFilesAtSource) {
deleteRequestUid(bruFile);
}
fs.rmSync(pathname, { recursive: true, force: true});
fs.rmSync(pathname, { recursive: true, force: true });
} else if (['http-request', 'graphql-request'].includes(type)) {
if(!fs.existsSync(pathname)) {
if (!fs.existsSync(pathname)) {
return Promise.reject(new Error('The file does not exist'));
}
@@ -283,13 +301,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
ipcMain.handle('renderer:open-collection', () => {
if(watcher && mainWindow) {
if (watcher && mainWindow) {
openCollectionDialog(mainWindow, watcher);
}
});
ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
if(watcher && mainWindow) {
if (watcher && mainWindow) {
console.log(`watcher stopWatching: ${collectionPath}`);
watcher.removeWatcher(collectionPath, mainWindow);
lastOpenedCollections.remove(collectionPath);
@@ -298,16 +316,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try {
let collectionName = collection.name;
let collectionName = sanitizeDirectoryName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)){
if (fs.existsSync(collectionPath)) {
throw new Error(`collection: ${collectionPath} already exists`);
}
// Recursive function to parse the collection items and create files/folders
const parseCollectionItems = (items = [], currentPath) => {
items.forEach(item => {
items.forEach((item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
const content = jsonToBru(item);
const filePath = path.join(currentPath, `${item.name}.bru`);
@@ -317,7 +335,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath);
if(item.items && item.items.length) {
if (item.items && item.items.length) {
parseCollectionItems(item.items, folderPath);
}
}
@@ -326,11 +344,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const parseEnvironments = (environments = [], collectionPath) => {
const envDirPath = path.join(collectionPath, 'environments');
if(!fs.existsSync(envDirPath)){
if (!fs.existsSync(envDirPath)) {
fs.mkdirSync(envDirPath);
}
environments.forEach(env => {
environments.forEach((env) => {
const content = envJsonToBru(env);
const filePath = path.join(envDirPath, `${env.name}.bru`);
fs.writeFileSync(filePath, content);
@@ -340,14 +358,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
const content = await stringifyJson({
const brunoConfig = {
version: '1',
name: collection.name,
name: collectionName,
type: 'collection'
});
};
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(collectionPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, collectionName);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid);
lastOpenedCollections.add(collectionPath);
@@ -355,7 +374,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// create folder and files based on collection
await parseCollectionItems(collection.items, collectionPath);
await parseEnvironments(collection.environments, collectionPath);
} catch (error) {
return Promise.reject(error);
}
@@ -363,11 +381,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
try {
for(let item of itemsToResequence) {
for (let item of itemsToResequence) {
const bru = fs.readFileSync(item.pathname, 'utf8');
const jsonData = bruToJson(bru);
if(jsonData.seq !== item.seq) {
if (jsonData.seq !== item.seq) {
jsonData.seq = item.seq;
const content = jsonToBru(jsonData);
await writeFile(item.pathname, content);
@@ -397,17 +415,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderName = path.basename(folderPath);
const newFolderPath = path.join(destinationPath, folderName);
if(!fs.existsSync(folderPath)) {
if (!fs.existsSync(folderPath)) {
throw new Error(`folder: ${folderPath} does not exist`);
}
if(fs.existsSync(newFolderPath)) {
if (fs.existsSync(newFolderPath)) {
throw new Error(`folder: ${newFolderPath} already exists`);
}
const bruFilesAtSource = await searchForBruFiles(folderPath);
for(let bruFile of bruFilesAtSource) {
for (let bruFile of bruFilesAtSource) {
const newBruFilePath = bruFile.replace(folderPath, newFolderPath);
moveRequestUid(bruFile, newBruFilePath);
}
@@ -422,9 +440,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();
if(lastOpened && lastOpened.length) {
for(let collectionPath of lastOpened) {
if(isDirectory(collectionPath)) {
if (lastOpened && lastOpened.length) {
for (let collectionPath of lastOpened) {
if (isDirectory(collectionPath)) {
openCollection(mainWindow, watcher, collectionPath, {
dontSendDisplayErrors: true
});
@@ -436,25 +454,39 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:set-preferences', async (event, preferences) => {
setPreferences(preferences);
});
ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionUid) => {
try {
const brunoConfigPath = path.join(collectionPath, 'bruno.json');
const content = await stringifyJson(brunoConfig);
await writeFile(brunoConfigPath, content);
} catch (error) {
return Promise.reject(error);
}
});
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
ipcMain.on('main:open-collection', () => {
if(watcher && mainWindow) {
if (watcher && mainWindow) {
openCollectionDialog(mainWindow, watcher);
}
});
ipcMain.on('main:open-docs', () => {
const docsURL = 'https://docs.usebruno.com';
shell.openExternal(docsURL);
});
ipcMain.on('main:collection-opened', (win, pathname, uid) => {
watcher.addWatcher(win, pathname, uid);
lastOpenedCollections.add(pathname);
});
}
};
const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {
registerRendererEventHandlers(mainWindow, watcher, lastOpenedCollections);
registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections);
}
};
module.exports = registerCollectionsIpc;

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