Compare commits

...

374 Commits

Author SHA1 Message Date
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
Anoop M D
19a7f397bb feat(#197): prettier formatting on all files in packages/bruno-app 2023-09-18 13:37:00 +05:30
Anoop M D
a103f41d85 Merge pull request #196 from mirkogolze/main
check response type for ResponsePane CodeEditor
2023-09-18 13:20:16 +05:30
Mirko Golze
7d4d1573af set content-type header only if not set by user 2023-09-18 07:12:18 +02:00
Mirko Golze
6f2bb55ecf format change 2023-09-16 22:32:35 +02:00
Mirko Golze
f189cb1a2e check response type to show XML not surrounded by quotation marks and with highlighting 2023-09-16 22:24:35 +02:00
Anoop M D
c020cd94a8 chore: updated @bruno/js version in electron 2023-09-15 17:15:15 +05:30
Anoop M D
0d4f7e6a06 chore: bumped release version to v0.14.0 2023-09-14 00:25:04 +05:30
Anoop M D
f9ed68843d feat(#190): Console Logs in scripts should be written into developer console 2023-09-14 00:20:04 +05:30
Anoop M D
d3a56fdc82 fix(#151, #188): fixed issue where collections vars in tests were not being updated 2023-09-13 23:58:16 +05:30
Anoop M D
e6c3a5af4c feat: refactored run single request events 2023-09-13 23:37:21 +05:30
Anoop M D
cee8073bb7 Merge branch 'main' of github.com:usebruno/bruno 2023-09-13 18:10:22 +05:30
Anoop M D
69be52cf9e chore: electron notarization for mac 2023-09-13 18:09:59 +05:30
Anoop M D
9e400085e3 Merge pull request #187 from BrentShikoski/feature/support-custom-cacert
support custom cacert file
2023-09-09 13:59:05 +05:30
Brent Shikoski
b07bb67943 consistent formatting 2023-09-08 21:33:09 -05:00
Brent Shikoski
593210456a refactor code around https.Agent for flexibility 2023-09-08 21:23:27 -05:00
Brent Shikoski
e328a4615e cacert support through process environment vars 2023-09-08 20:56:12 -05:00
Brent Shikoski
18afb73238 support custom cacerts in electron app 2023-09-08 20:53:18 -05:00
Anoop M D
73b71e0829 release(#183): bruno cli v0.7.1 2023-09-07 12:31:21 +05:30
Anoop M D
4c25aa99aa Merge pull request #183 from BrentShikoski/feature/support-custom-cacert
support alternative cacert file in cli
2023-09-07 12:06:13 +05:30
Anoop M D
23843b5d0a chore: temporarily disabling mac notarization 2023-09-06 22:30:45 +05:30
Anoop M D
7fbd338fa6 feat: bru.getEnvName() 2023-09-06 20:36:55 +05:30
Anoop M D
4aeb5cf56d Merge pull request #182 from jeffprinty/feature/sidebar-collection-scrolling
Enable scrolling in collections sidebar #118
2023-09-05 12:42:36 +05:30
Brent Shikoski
1ed39a5ea6 support alternative cacert 2023-09-04 15:49:36 -05:00
Jeff Printy
74f248782b Enable scrolling in collections sidebar #118 2023-09-04 14:21:59 -05:00
Anoop M D
99239e19b4 release bruno cli v0.7.0 2023-09-04 12:24:18 +05:30
Anoop M D
f46160e161 Merge pull request #181 from BrentShikoski/feature/cli-certificate-validation
add ability to turn certificate validation off in the cli
2023-09-04 12:09:28 +05:30
Brent Shikoski
d0147778db add ability to turn certificate validation off 2023-09-03 20:43:32 -05:00
Brent Shikoski
87119cee2e add ability to turn certificate validation off 2023-09-03 20:19:56 -05:00
Anoop M D
b25d896dd6 fix(#178): fixed await issue while running scripts 2023-09-01 17:49:34 +05:30
Anoop M D
08495e7fb5 fix(#178): fixed issue in bruno cli where collection run was getting aborted for 4xx/5xx responses 2023-09-01 14:18:41 +05:30
Anoop M D
ee084696f5 chore: release v0.13.2 2023-09-01 14:17:29 +05:30
Anoop M D
aeb9fc8875 chore: release bruno packages to npm 2023-09-01 13:43:26 +05:30
Anoop M D
26a05f92cb chore: bumped version v0.13.1 2023-09-01 00:15:14 +05:30
Anoop M D
fff7819c46 fix(#177): fixed issue where collection run was getting aborted after 4xx/5xx response 2023-09-01 00:13:31 +05:30
Anoop M D
1d678ee0d9 chore: bump version to 0.13.0 2023-08-31 23:03:34 +05:30
Anoop M D
fd4c188c95 feat(#177 #165): running tests and assertions for error responses 2023-08-30 21:35:59 +05:30
Anoop M D
97678b05fc Merge branch 'main' of github.com:usebruno/bruno 2023-08-30 20:31:57 +05:30
Anoop M D
a1c9625aee feat(#168): disable ssl option 2023-08-30 20:31:15 +05:30
Anoop M D
a9d74467ff Merge pull request #176 from LesageYann/chore/doc-improve-suggest
chore: improve the cli doc to avoid misunderstandings
2023-08-29 20:05:55 +05:30
Lesage Yann
bd670eceb6 chore: improve the cli doc to avoid misunderstandings on the command: 2023-08-29 11:36:35 +02:00
Anoop M D
1ccb66e92a Merge pull request #173 from dcoomber/feature/48-husky-prettier
Added prettier precommit hook with husky Re #48
2023-08-29 10:47:09 +05:30
Anoop M D
d62881fe0d feat: updated electron builder config to use js based config 2023-08-28 23:14:40 +05:30
David Coomber
9ea95b4571 Copy prettier config from packages/bruno-app Re #48 2023-08-19 10:40:54 +02:00
David Coomber
df9322b767 Added prettier precommit hook with husky Re #48 2023-08-19 09:20:46 +02:00
Anoop M D
c27c750c3e feat: preferences local storage and electron sync 2023-08-19 00:36:37 +05:30
Anoop M D
94baee8e25 feat: Preferences (General Tab) 2023-08-18 01:09:00 +05:30
Anoop M D
417b50b0ad feat: Preferences (Theme and Support) 2023-08-18 00:18:30 +05:30
Anoop M D
bf5ee7e409 Merge pull request #171 from dcoomber/feature/process-env-secrets
RFC: Support process.env in Bru script Re #170
2023-08-17 23:00:41 +05:30
David Coomber
3ca0107f1b Proposal to support process.env in Bru script Re #170 2023-08-11 19:48:09 +02:00
Anoop M D
ec22fdb637 chore: initial commit for bruno docs package 2023-05-19 19:57:58 +05:30
Anoop M D
66024d04e9 chore: version bump 2023-05-02 15:26:27 +05:30
Anoop M D
330a7ad18a fix: fixed bug where content type was getting overwritten everytime as application/x-www-form-urlencoded 2023-05-02 15:25:07 +05:30
Anoop M D
2976842588 chore: version bump for bruno cli v0.5.0 2023-04-30 12:10:08 +05:30
Anoop M D
51e0ea2c2d feat(#155): exit process with non zero code when tests or assertions fail 2023-04-30 12:08:23 +05:30
Anoop M D
3b85e7ebcc chore: version bump v0.12.1 2023-04-27 19:26:41 +05:30
Anoop M D
49b0f3a322 fix(#148) : fixed issue where form url encoded params were not being interpolated 2023-04-27 19:25:52 +05:30
Anoop M D
45ca5ded96 chore: bumped version to v0.12.0 2023-04-20 11:54:36 +05:30
Anoop M D
b6528062f0 fix: fixed issue where postman collection import was failing when the filename had / or ? chars (#147) 2023-04-20 11:46:55 +05:30
Anoop M D
86094cc054 fix: fixed issue where cancelling requests was throwing an error (#146) 2023-04-20 11:19:12 +05:30
Anoop M D
8e0bc68ada feat: support node-fetch as an inbuilt library (#138) 2023-04-20 10:51:29 +05:30
Anoop M D
c36c7b44a6 Merge pull request #145 from DivyMohan14/feature/async-pre-request-scripts
Adding support for using async pre-request scripts
2023-04-20 09:55:06 +05:30
Divy Mohan Rai
0ac27dee56 feature(async-script): adding support for using async pre-request scripts 2023-04-15 12:33:45 +05:30
Anoop M D
ede122ab09 fix: fix image rendering issues in bru cli npm readme 2023-04-01 13:57:59 +05:30
Anoop M D
96e368cb18 fix: fixed bru cli typos 2023-04-01 13:55:32 +05:30
Anoop M D
942b75861c fix: fixed broken link in bruno cli readme 2023-04-01 13:54:33 +05:30
Anoop M D
c1711ea01b fix: fixing npm image rendering issues in readme 2023-04-01 13:53:32 +05:30
Anoop M D
dedfefbc9a chore: release cli docs to npm 2023-04-01 13:48:35 +05:30
Anoop M D
65dd5df87e chore: added screenshot of bru cli output 2023-04-01 13:46:13 +05:30
Anoop M D
78d2393686 chore: prep for bruno-cli release 2023-04-01 13:40:23 +05:30
Anoop M D
0b65c4580e chore: updated image 2023-03-30 11:04:20 +05:30
Anoop M D
9cc1bf1e2f chore: updated readme 2023-03-30 10:56:37 +05:30
Anoop M D
b96e3d0f23 release: v0.11.0 2023-03-29 13:00:03 +05:30
Anoop M D
11c99d55dc fix: fixed yml indentation issue 2023-03-29 12:58:03 +05:30
Anoop M D
d054dc4c78 feat(#105): github workflow for release updates to homebrew 2023-03-29 12:55:49 +05:30
Anoop M D
9014dc5769 fix: fixed dark mode styling issue in assertions op selector 2023-03-29 12:49:17 +05:30
Anoop M D
d346970241 fix: fixed issue where unsaved changes where not being picked up while running tests (#125) 2023-03-29 12:43:47 +05:30
Anoop M D
bdb3051c2b fix: fixed issue where env vars was not getting embedded inside xml req body (#141) 2023-03-29 12:22:42 +05:30
Anoop M D
f8325b22b3 fix: fixed issue where env vars was not getting embedded inside xml req body (#141) 2023-03-29 12:20:40 +05:30
Anoop M D
78251c530c feat: added custom assertion for chaijs for match() method 2023-03-23 21:36:35 +05:30
Anoop M D
dea95664b9 fix: fixed issue in bru cli where assertions was not being run 2023-03-23 21:35:41 +05:30
Anoop M D
fbc6e7bff5 Merge pull request #135 from dcoomber/bugfix/132-isjson-assertion
Resolve issue with to.be.json assertions Re #132
2023-03-23 14:11:55 +05:30
David Coomber
4884106aaa Removed chai-http Re #132 2023-03-22 22:12:17 +02:00
David Coomber
5c15438949 Updated plugin to be addProperty Re #132 2023-03-22 20:56:35 +02:00
Anoop M D
b53a9eaee9 Merge pull request #134 from dcoomber/bugfix/128-close-tab-hotkey
Proposed addition of CMD+W hotkey Re #128
2023-03-21 22:26:28 +05:30
David Coomber
5899ca446d Applied code review feedback Re #128 2023-03-21 17:45:26 +02:00
David Coomber
d21e7f6fb5 Added Chai.js plugin to cater for isJson assertion Re #132 2023-03-21 17:30:45 +02:00
Anoop M D
ee8a3eae8c Merge pull request #130 from dcoomber/bugfix/request-dialog-terminology
Proposed adjustment to terminology on requests
2023-03-21 01:34:19 +05:30
Anoop M D
fac5109242 Merge pull request #136 from dcoomber/bugfix/dev-docs
Corrected reference to bruno-query node script
2023-03-21 01:33:24 +05:30
David Coomber
47dfbd2a64 Corrected reference to bruno-query node script 2023-03-19 21:14:52 +02:00
David Coomber
074d72d885 Add chai-http to enable to.be.json assertions Re #132 2023-03-19 21:08:19 +02:00
David Coomber
8c29d131e2 Proposed addition of CMD+W hotkey Re #128 2023-03-19 18:38:08 +02:00
David Coomber
437044bdcd Applied code review feedback 2023-03-19 17:17:38 +02:00
Anoop M D
2120a562da chore: improved dev documentation 2023-03-19 15:41:18 +05:30
Anoop M D
04c3c2dbf1 Merge pull request #133 from bharathbdev/bugfix/assertion-result-issue
Bugfix/assertion result issue
2023-03-19 14:57:19 +05:30
David Coomber
1d03e1d5ea Adjusted terminology on requests (REST, GraphQL, Form URL encoded) 2023-03-18 10:53:49 +02:00
Bharath B
2b174e1c60 added the indentation 2023-03-18 13:43:16 +05:30
Bharath B
7a2b32069e bugfix/assertion-result-issue fixed the issue related to assertions still displayed in Tests tab after deletion#121 2023-03-18 12:06:20 +05:30
Anoop M D
a9e6c3a35c feat: support for importing insomnia collections (#74) 2023-03-05 00:19:03 +05:30
Anoop M D
e6a754b933 Merge pull request #108 from ajaishankar/feature/object-predicate
filter shortcut for scalar properties
2023-02-27 21:32:59 +05:30
Ajai Shankar
ee4509f037 feat(query): simple object predicate for scalar properties 2023-02-26 12:56:11 -06:00
Anoop M D
c04f0e7a71 chore: added docs link 2023-02-26 17:26:06 +05:30
Anoop M D
2f52ce4c71 feat: windows codesigning 2023-02-26 17:22:30 +05:30
Anoop M D
b1edaba1c6 fix: fixed issue in react hook order during search (#106) 2023-02-26 14:51:55 +05:30
Anoop M D
3f6fcdd582 Merge branch 'main' of github.com:usebruno/bruno 2023-02-23 12:37:47 +05:30
Anoop M D
c745786b1c chore: release v0.10.1 2023-02-23 12:37:34 +05:30
Anoop M D
9e30c7b440 feat: vars and asserts in gql request UI 2023-02-23 11:42:25 +05:30
Anoop M D
b87cc7ccae Merge pull request #104 from dcoomber/feature/update-development-doc
Added snippet to development.md
2023-02-22 23:48:07 +05:30
David Coomber
1595d736f2 Added snippet to assist in deleting node_modules / package-lock.json in dir structure 2023-02-22 20:08:48 +02:00
Anoop M D
b38c25ca70 feat: mac signinging and notarization 2023-02-22 19:15:37 +05:30
Anoop M D
f22858219b fix: fixed issue while deleting empty query params (#93) 2023-02-22 02:42:59 +05:30
Anoop M D
8044286b80 feat: integrated assert runtime for ui 2023-02-22 02:25:02 +05:30
Anoop M D
34a2e23dc6 feat: assertion operator in UI 2023-02-22 01:20:07 +05:30
Anoop M D
224b8c3cc4 feat: vars runtime in UI 2023-02-21 15:26:12 +05:30
Anoop M D
d58e92205b feat: assertions implementation in UI 2023-02-21 14:04:05 +05:30
Anoop M D
925af1f26f feat: vars implementation in UI 2023-02-21 13:05:51 +05:30
Anoop M D
d07744d5c2 chore: deleted unused chrome extension package 2023-02-21 00:22:20 +05:30
Anoop M D
5efb18ad63 chore: npm publish 2023-02-21 00:00:10 +05:30
Anoop M D
9cfb54ee9f Merge pull request #91 from ajaishankar/feature/get-supercharged
res.get : deep object navigation and filtering
2023-02-20 19:35:10 +05:30
Anoop M D
4c9d22d1e0 Merge pull request #99 from dcoomber/bugfix/createcollection-tab-order
Correct the tab order on the CreateCollection modal
2023-02-20 14:53:42 +05:30
Ajai Shankar
c5d43cc9e6 chore: add bruno-query test/build to github workflows 2023-02-19 23:51:47 -06:00
Ajai Shankar
8300830a95 Merge branch 'main' into feature/get-supercharged 2023-02-19 23:38:27 -06:00
Ajai Shankar
2dfc972930 feat: res default to bruno query 2023-02-19 23:35:49 -06:00
Ajai Shankar
4fdfdaf2cb feat(query): bruno-query package 2023-02-19 22:48:34 -06:00
David Coomber
a1385ba1e2 Location 'inputRef' was overriding the same on Name 2023-02-18 13:02:59 +02:00
Anoop M D
15804ac293 chore: updated bruno schema version 2023-02-17 14:20:36 +05:30
Anoop M D
0244b2e1d6 Merge pull request #98 from dcoomber/bugfix/contributing
Removed redundant instructions in docs
2023-02-17 14:06:05 +05:30
Anoop M D
7e70d05dc8 chore: bumped version to v0.9.4 2023-02-17 13:58:02 +05:30
Anoop M D
e60b06e4a4 chore: npm publish 2023-02-17 13:57:06 +05:30
Anoop M D
17ded5de4c fix: fixed issues with creating patch requests 2023-02-17 13:55:23 +05:30
Anoop M D
e1b97643bd fix: fixed issue with separators in electron menu #92 2023-02-17 13:47:13 +05:30
Anoop M D
8103554545 fix: disable app reload #94 2023-02-17 13:39:05 +05:30
Anoop M D
a425b42615 feat: ux improvements in environment settings 2023-02-17 13:36:22 +05:30
Anoop M D
b14f867811 chore: publish npm packages 2023-02-17 12:59:30 +05:30
Anoop M D
013abeaa80 fix: fixed parser issue related to env variables #97 2023-02-17 12:56:48 +05:30
David Coomber
9d3762702f Removed redundant local dev environment instructions 2023-02-16 21:22:35 +02:00
Anoop M D
cac9f9aef4 chore: updated dev docs 2023-02-16 01:59:07 +05:30
Anoop M D
2b63368f2c chore: updated dev docs 2023-02-16 01:58:11 +05:30
Anoop M D
acd980ffc6 chore: updated dev docs 2023-02-16 01:56:38 +05:30
Anoop M D
1a175e4449 chore: added bruno-js unit tests to github workflows 2023-02-13 13:11:56 +05:30
Ajai Shankar
209f30998e test: minor 2023-02-12 19:47:14 -06:00
Ajai Shankar
e777eed00d feat(get): supercharged res getter 2023-02-12 17:27:54 -06:00
Anoop M D
15fc24679c chore: release v0.9.3 2023-02-12 22:05:38 +05:30
Anoop M D
48d26c05d9 fix: fix windows filepath issues #89 2023-02-12 21:59:20 +05:30
Anoop M D
9d395ded33 Merge branch 'main' of github.com:usebruno/bruno 2023-02-12 21:46:58 +05:30
Anoop M D
943e74c327 fix: fix windows filepath issues #89 2023-02-12 21:46:42 +05:30
Anoop M D
b852d1cc52 Merge pull request #90 from ajaishankar/feature/expression-eval
Compiled and cached expressions
2023-02-11 22:57:17 +05:30
Ajai Shankar
3d22f77226 feat(eval): handle globals 2023-02-11 08:57:27 -06:00
Ajai Shankar
429ca4093c test: expression cache 2023-02-10 23:34:46 -06:00
Ajai Shankar
a4f757ee87 minor: clear expression cache before and after test 2023-02-10 22:24:28 -06:00
Ajai Shankar
df4f322024 feat(eval): compiled and cached expressions 2023-02-10 21:55:05 -06:00
Anoop M D
ddd39e630d chore: release v0.9.2 2023-02-09 17:56:25 +05:30
Anoop M D
ef8e8bf637 feat: improved error messaging while attempting to create duplicate requests and folders 2023-02-09 17:55:42 +05:30
Anoop M D
7405fa9709 chore: publish npm packages 2023-02-09 17:33:21 +05:30
Anoop M D
242fcac2d3 feat: bru lang now allows empty urls 2023-02-09 17:31:37 +05:30
Anoop M D
efd15838aa fix: fixed bug where gql imports were not working 2023-02-09 15:11:05 +05:30
Anoop M D
c55f9d42da feat: better error handling in bru cli 2023-02-08 18:27:33 +05:30
Anoop M D
2f32f7024e chore: npm publish 2023-02-08 18:19:26 +05:30
Anoop M D
aff6499478 feat: assert tab allows any valid js code as keys 2023-02-08 18:17:30 +05:30
Anoop M D
45ed47ff90 chore: added website link in readme 2023-02-08 16:47:52 +05:30
Anoop M D
27c6c1349a chore: release v0.9.1 2023-02-08 16:27:33 +05:30
Anoop M D
c78ffa3a80 chore: npm publish 2023-02-08 16:26:11 +05:30
Anoop M D
6b2d335ade fix: fixed string length comparision bug in assert runtime 2023-02-08 05:10:14 +05:30
Anoop M D
837e39d870 fix: fixed bugs in bru cli related to gql requests 2023-02-08 04:13:22 +05:30
Anoop M D
67643c4c48 chore: publishing to npm 2023-02-08 03:45:27 +05:30
Anoop M D
2b384656b6 feat: bruno can run a collection by specifying "bru run" 2023-02-08 03:27:27 +05:30
Anoop M D
411c06f4cb feat: bruno cli can not run a folder recursively 2023-02-08 02:53:55 +05:30
Anoop M D
03fa46d8b3 feat: bruno cli can sort the requests being run 2023-02-08 01:43:16 +05:30
Anoop M D
d0f2eb27bc feat: bru cli can now run all requests inside a directory 2023-02-08 01:25:15 +05:30
Anoop M D
1b9ec05a58 feat: assert runtime 2023-02-08 01:13:21 +05:30
Anoop M D
3f74178c81 feat: bru cli - specify env + completed vars runtime 2023-02-07 21:01:35 +05:30
Anoop M D
78ca6c5e96 feat: error messages for reserved file and folder names in bruno 2023-02-07 19:36:34 +05:30
Anoop M D
5f59a16090 feat: run again, run collection and close functionality in collection runner 2023-02-07 19:00:17 +05:30
Anoop M D
3805cef0c4 feat: auto focus newly created environment 2023-02-07 18:11:34 +05:30
Anoop M D
3c1a6ca71e chore: release v0.9.0 2023-02-07 08:15:13 +05:30
Anoop M D
d2227b2b05 feat: renamed vars:req,res as vars:pre-request,post-response 2023-02-07 05:13:14 +05:30
Anoop M D
dc03b6a761 feat: renamed script:req,res as script:pre-request,post-response 2023-02-07 04:39:23 +05:30
Anoop M D
e22f164cbc feat: simple vars runtime is working! 2023-02-07 04:33:25 +05:30
Anoop M D
580d681e0a fix: fixing issues in bru cli 2023-02-07 02:58:44 +05:30
Anoop M D
89b721d726 fix: fixed issues around body mode conversion 2023-02-07 02:50:15 +05:30
Anoop M D
1110a4edda fix: fixed gql related issues 2023-02-07 02:12:23 +05:30
Anoop M D
f69332d9c3 feat: automagically migrate users of bru v1 to bru v2 2023-02-07 01:19:32 +05:30
Anoop M D
6947860204 feat: made bru lang parser more robust to optional newlines and whitespaces 2023-02-07 01:18:18 +05:30
Anoop M D
963b0c257f feat: integrate new env model of bru lang 2023-02-06 23:22:48 +05:30
Anoop M D
33f8900705 chore: cleanup unused files 2023-02-06 23:02:47 +05:30
Anoop M D
22a14aa67a feat: making request and response scripts work 2023-02-06 23:00:50 +05:30
Anoop M D
60c96f7d27 feat: script and vars are segmented at req and res levels separately 2023-02-06 21:18:36 +05:30
Anoop M D
c8de57aa51 chore: restructure bru js package 2023-02-06 15:24:34 +05:30
Anoop M D
827c480689 feat: bru cli prints test results 2023-02-06 14:52:22 +05:30
Anoop M D
1c869013c6 feat: cli runner can now run a single request 2023-02-06 03:40:13 +05:30
Anoop M D
404a516fef chore: bruno cli accept request filename 2023-02-06 02:57:59 +05:30
Anoop M D
e26075060e chore: bru cli - added package deps 2023-02-06 02:34:27 +05:30
Anoop M D
c524f40ab2 feat: bru cli init 2023-02-06 02:27:22 +05:30
Anoop M D
3e563ea126 feat: bru lang - support body default as json 2023-02-06 01:27:08 +05:30
Anoop M D
a0cb53445f feat: bru lang - supporting ~ @ identifiers 2023-02-05 23:13:18 +05:30
Anoop M D
84bd603e11 feat: bru lang - parse env files 2023-02-05 19:06:48 +05:30
Anoop M D
c3236d4eb1 feat: making changes in app to use the new bru lang format 2023-02-05 01:25:36 +05:30
Anoop M D
4a4208f272 feat: bru lang - jsonToBru functionality 2023-02-05 00:27:18 +05:30
Anoop M D
d24f1a1054 refactor: organized v1 and v2 versions inside bru-lang 2023-02-04 20:11:33 +05:30
Anoop M D
86200a8f11 Merge pull request #85 from usebruno/feature/bru-lang-parser
Bru Lang Parser
2023-02-04 20:02:34 +05:30
Anoop M D
cf0ede1a83 chore: using fixtures to cleanup test file 2023-02-04 16:11:29 +05:30
Anoop M D
342a39bcb4 chore: renamed test files 2023-02-04 16:06:32 +05:30
Anoop M D
e7d332c7d7 feat: bru lang - support for vars, asserts and docs 2023-02-04 16:02:27 +05:30
Anoop M D
689d886e74 feat: bru lang - support for body type parsing 2023-02-04 06:06:02 +05:30
Anoop M D
7a8e5198ff feat: bru lang - support disabled headers parsing 2023-02-03 23:27:06 +05:30
Anoop M D
118ceacf46 feat: bru lang - allow parsing empty header values 2023-02-03 23:02:16 +05:30
Anoop M D
a21615a5fb feat: bru lang - keys can support any char except whitespace, values can have any char except newline 2023-02-03 21:44:07 +05:30
Anoop M D
2ee2e270b0 feat: brun lang - ast updates, tests for headers and script tags 2023-02-03 21:08:40 +05:30
Anoop M D
9d6ba4691c feat: bru lang tests, scripts and headers using ohm 2023-02-03 08:01:44 +05:30
Anoop M D
104bd272f9 feat: bru lang - simple parser 2023-02-03 04:39:45 +05:30
Anoop M D
62a184c386 chore: fixed github star button alignment 2023-02-01 22:20:06 +05:30
Anoop M D
4663a1246c release: v0.8.1 2023-02-01 21:54:11 +05:30
Anoop M D
0efd782bcb feat: github star button blends with dark mode 2023-02-01 21:52:46 +05:30
Anoop M D
a0903a5842 fix: fixed many bugs (too many to count :) ) 2023-02-01 21:21:21 +05:30
Anoop M D
8202182074 release time: v0.8.0 2023-02-01 18:09:27 +05:30
Anoop M D
ee4d4e3361 chore: fixed runner layout issues 2023-02-01 18:03:43 +05:30
Anoop M D
b76ddcd007 feat: scripting and testing support in graphql has arrived 2023-02-01 18:02:10 +05:30
Anoop M D
6f6dedbb9c feat: collection variables 2023-02-01 17:56:13 +05:30
Anoop M D
37b1c043eb feat: start collection runner at root 2023-02-01 17:29:53 +05:30
Anoop M D
58bc247c53 feat: collection runner 2023-02-01 17:06:04 +05:30
Anoop M D
c5b509115a chore: fixed collection chevron width issue 2023-02-01 10:07:11 +05:30
Anoop M D
524a59aed4 chore: hardened dnd boundary 2023-02-01 10:05:09 +05:30
Anoop M D
be49ef5f12 feat: rename collection 2023-02-01 09:59:23 +05:30
Anoop M D
d4f05fa843 feat: support for graphql variables 2023-02-01 09:23:11 +05:30
Anoop M D
adedd08e8a fix: relax schema validation max values in bruno-schema 2023-02-01 08:47:30 +05:30
Anoop M D
7dd0d10a5d fix: fixed bugs related to sequencing 2023-02-01 08:37:48 +05:30
Anoop M D
d9ef1692fe feat: generic key val line parser 2023-02-01 08:00:10 +05:30
Anoop M D
b88848f0dc chore: deleted unused workspace schema 2023-02-01 06:35:45 +05:30
Anoop M D
abc26e5c5a feat: load current environment during configuring envs 2023-02-01 06:31:32 +05:30
Anoop M D
d7733552bf chore: github workflow now runs bruno-app tests 2023-02-01 06:08:50 +05:30
Anoop M D
6852cc6631 feat: refactored logic around query param parsing 2023-02-01 06:07:43 +05:30
Anoop M D
8bfb2591c2 feat: graphql schema introspection 2023-02-01 05:09:42 +05:30
Anoop M D
05a290839b fix: fixed sidebar toggle width issues 2023-02-01 03:25:46 +05:30
Anoop M D
80f9e33be5 fix: fix overflow issues in keyval editors in the app 2023-02-01 03:06:32 +05:30
Anoop M D
5a78dfa210 fix: fixed dark mode theme issue in single line editor 2023-01-31 21:38:11 +05:30
Anoop M D
61caca59ee fix: fixed scrollbar issue in single line editor 2023-01-31 21:25:25 +05:30
Anoop M D
383c5ba782 chore: fixed typo in readme 2023-01-30 00:43:30 +05:30
Anoop M D
28fbaa3470 chore: adjusted logo image width in readme 2023-01-30 00:41:56 +05:30
Anoop M D
27dcf78e73 chore: updated readme documentation 2023-01-30 00:39:39 +05:30
Anoop M D
25883b84fa feat: added badges in readme 2023-01-30 00:28:42 +05:30
Anoop M D
667811cbd4 bumped version 2023-01-29 20:45:30 +05:30
Anoop M D
7839e93a57 fix: fixed dark mode issues 2023-01-29 20:44:34 +05:30
Anoop M D
11c60273b4 fix: fixed missing module error 2023-01-29 19:52:13 +05:30
Anoop M D
1dcff56c78 release: v0.7.0 2023-01-29 19:27:44 +05:30
Anoop M D
2e32423869 feat: better error messaging 2023-01-29 18:04:17 +05:30
Anoop M D
c328281f21 feat: testing support has arrived ! 2023-01-29 17:35:28 +05:30
Anoop M D
cc261326fc fix: fixed env var issues 2023-01-29 14:17:01 +05:30
Anoop M D
050ee2680f feat: improved request queuing status functionality 2023-01-29 13:20:19 +05:30
Anoop M D
b2c28465e9 fix: patch bug temporarily 2023-01-29 13:09:33 +05:30
Anoop M D
cd36335c60 feat: support crypto-js as an inbuilt library 2023-01-29 12:34:37 +05:30
Anoop M D
d89f12c071 feat: support loading external libraries 2023-01-29 12:33:12 +05:30
Anoop M D
905f459ed0 feat: support for inbuilt libraries during scripting 2023-01-29 11:12:34 +05:30
Anoop M D
b800055df4 chore: hiding menubar icon temporarily 2023-01-29 11:12:11 +05:30
Anoop M D
b1d2b798ba feat: scripting support almost done 2023-01-29 04:49:31 +05:30
Anoop M D
4a403a253e feat: show current env vars 2023-01-27 03:24:21 +05:30
Anoop M D
a45628dd85 feat: moved env var interpolation logic to electron 2023-01-25 10:39:07 +05:30
Anoop M D
977637e556 chore: updated manifesto in readme 2023-01-24 21:44:33 +05:30
Anoop M D
3d63db806d feat: moved prepare request logic to electron 2023-01-24 19:34:06 +05:30
Anoop M D
1ec24d1138 feat: scripting support (#16) 2023-01-24 18:27:47 +05:30
Anoop M D
fa40685a6a feat: skipping telemetry in dev env 2023-01-24 17:28:31 +05:30
Anoop M D
037013005f chore: bumped version 2023-01-23 00:18:54 +05:30
Anoop M D
0c42298ce6 feat: deprecated form-url-encoded in favour of form-urlencoded in body type in bru lang 2023-01-22 23:49:25 +05:30
Anoop M D
84ce75263b chore: added bruno-schema tests to github workflow 2023-01-22 23:39:59 +05:30
Anoop M D
5c8d0a9e8a feat: script and tests functionality 2023-01-22 23:39:16 +05:30
Anoop M D
b70bbf78b1 chore: renamed file 2023-01-22 18:35:57 +05:30
Anoop M D
43b9412ddb chore: rename package 2023-01-22 18:32:28 +05:30
Anoop M D
b56972fd93 chore: updated readme 2023-01-22 06:02:00 +05:30
Anoop M D
c102ac527a chore: updated readme 2023-01-22 06:00:00 +05:30
Anoop M D
45229b1af7 chore: version bump 2023-01-22 02:50:24 +05:30
Anoop M D
f9a3fb2f1b chore: cleanup old files 2023-01-22 02:43:14 +05:30
Anoop M D
65d8a707d8 chore: updated readme 2023-01-22 02:41:31 +05:30
Anoop M D
cc6bf45d5f chore: updated dev docs 2023-01-22 02:38:59 +05:30
Anoop M D
8fbb777665 feat: ask foldername when creating collection 2023-01-22 02:34:23 +05:30
Anoop M D
0e041d460c feat: listeners for cmd+s ctr+s in SingleLineEditor 2023-01-22 00:49:11 +05:30
Anoop M D
2e3b296021 feat: making regex in bruno lang support windows line endings 2023-01-22 00:38:10 +05:30
Anoop M D
405b50edcd fix: support parsing of empty urls in bru files 2023-01-22 00:35:58 +05:30
Anoop M D
fff540010e feat: hardening seq number functionality 2023-01-21 23:22:45 +05:30
Anoop M D
e513694912 fix: fixed seq type bug in bruno lang parser 2023-01-21 19:56:39 +05:30
Anoop M D
d01cada16c fix: fix json stringify bug in response timeline 2023-01-21 19:54:06 +05:30
Anoop M D
dd4fecfd1c fix: fix env info popup issues in graphql query editor 2023-01-21 19:41:37 +05:30
Anoop M D
095d7c6bcb feat: dark mode styles for env var info popup 2023-01-21 19:07:58 +05:30
Anoop M D
d165a04377 feat: environment variable syntax highlighting 2023-01-21 18:12:34 +05:30
Anoop M D
d3d1e47950 fix: fixed unit tests 2023-01-21 01:56:30 +05:30
Anoop M D
9c14941c15 feat: using single line editors instead of input boxes 2023-01-21 01:42:20 +05:30
Anoop M D
1627f65bd7 feat: using single line editors instead of input boxes 2023-01-21 01:38:48 +05:30
Anoop M D
19f4f3c1a5 chore: deprecating descriptions temporarily in key value pairs 2023-01-21 01:26:40 +05:30
Anoop M D
ae70680ceb feat: ener keybindings for single line editor 2023-01-21 01:23:33 +05:30
Anoop M D
60fc13c765 feat: refactor codemirror bruno variables mode 2023-01-21 01:17:27 +05:30
Anoop M D
60c3d41c8e feat: codemirror single line editor 2023-01-20 09:39:32 +05:30
Anoop M D
fb8ff37d83 feat: codemirror syntax highlight for env vars 2023-01-20 08:14:03 +05:30
Anoop M D
0d9b30e730 chore: updated .bru examples 2023-01-20 07:23:31 +05:30
Anoop M D
695f42df80 feat: run request upon cmd+enter from response pane 2023-01-20 07:16:57 +05:30
Anoop M D
6b43159be2 chore: cleanup 2023-01-20 03:27:01 +05:30
Anoop M D
21c9c8b4fb feat: drag and drop for files and folders 2023-01-20 00:45:07 +05:30
Anoop M D
c4abe54c3f fix: disable watcher updates on env directories 2023-01-19 02:00:02 +05:30
Anoop M D
dd71c9e71b feat: response timeline 2023-01-18 20:55:10 +05:30
Anoop M D
2be3e4bf69 feat: yay node v14 to v18 2023-01-18 20:53:27 +05:30
Anoop M D
f34e9f7b26 chore: fixed typo 2023-01-18 14:47:43 +05:30
Anoop M D
76b0729af3 feat: ditched web, all in on desktop app 2023-01-18 04:11:42 +05:30
Anoop M D
4877bc3849 chore: updated product tagline 2023-01-17 19:42:34 +05:30
Anoop M D
0742e3415c feat: github workflow for running unit tests 2023-01-17 19:35:59 +05:30
Anoop M D
ae7e3a722c chore: disable dragndrop temporarily due to electron issues 2023-01-17 19:27:11 +05:30
Anoop M D
7f2e19250f feat: generate collection hash on the fly 2023-01-17 19:26:41 +05:30
Anoop M D
4e16e954ef Merge branch 'main' of github.com:usebruno/bruno 2023-01-17 02:01:36 +05:30
Anoop M D
b6c3205474 feat: integrating app with the bru lang 2023-01-17 02:00:58 +05:30
Anoop M D
23076b41c6 feat: bruno land outdent strings during parsing 2023-01-17 00:55:47 +05:30
Anoop M D
b5116b54af feat: fix bugs in bruno-lang in data format 2023-01-17 00:20:22 +05:30
A-childs-encyclopedia
83aaa21b5b Update readme.md (#77) 2023-01-16 09:23:28 +05:30
Anoop M D
e1e7b37ce5 feat: bruno lang support for stringify json into bru file 2023-01-16 00:49:06 +05:30
Anoop M D
8dab9268f2 feat: bruno lang now supports parsing text only multipart form data 2023-01-15 23:02:59 +05:30
Anoop M D
4eed999db1 feat: bruno lang now supports parsing for url encoded params 2023-01-15 22:42:56 +05:30
Anoop M D
c29ab50a3d chore: bruno lang improve parsing logic of headers and params 2023-01-15 05:04:35 +05:30
Anoop M D
5e1d6cba4a feat: bruno lang support for parsing xml body 2023-01-15 05:03:58 +05:30
Anoop M D
a645d1459c feat: bruno lang - support parsing text body 2023-01-15 04:03:52 +05:30
Anoop M D
24e11a864c feat: bruno lang - parse graphql body 2023-01-15 03:51:48 +05:30
Anoop M D
87a4778a91 feat: bruno-lang now supprts parsing body json 2023-01-15 00:45:01 +05:30
Anoop M D
0750af4c68 chore: added examples inside bruno-lang 2023-01-15 00:44:21 +05:30
Anoop M D
60e613fac8 feat: bruno lang support parsing headers in .bru file 2023-01-14 20:21:54 +05:30
Anoop M D
b75baf57ba feat: bruno lang parse .bru file 2023-01-14 20:16:09 +05:30
Anoop M D
137df3c5c0 feat: bruno lang inline tag parser 2023-01-14 16:53:52 +05:30
Anoop M D
6ef2daebbd chore: updated bru files formatting 2023-01-14 04:37:39 +05:30
Anoop M D
55f85e3728 chore: updated bru files 2023-01-14 04:21:57 +05:30
Anoop M D
f0269069d2 feat: brun-lang package init 2023-01-12 23:46:01 +05:30
Anoop M D
61dbca3243 feat: highlight js inside script block in .bru files 2023-01-10 10:58:47 +05:30
Anoop M D
f21cb240c4 feat: vscode extension for bruno 2023-01-10 10:07:30 +05:30
Anoop M D
ca46e14732 feat: bru-file package init 2023-01-10 09:45:24 +05:30
Anoop M D
87f6000b85 Merge branch 'feature/sort-requests' 2023-01-10 09:39:13 +05:30
Anoop M D
36d0550472 feat: drag item to root of collection 2023-01-10 09:37:09 +05:30
Anoop M D
ee4734c957 feat: move requests across folders 2022-12-28 04:48:49 +05:30
Anoop M D
02f9fc0a7b fix: fixed mac electron build issues 2022-12-22 00:34:10 +05:30
Anoop M D
6ce657d891 feat: drag and drop events (#57) 2022-11-11 03:53:51 +05:30
375 changed files with 16157 additions and 5231 deletions

View File

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

29
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Unit Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: 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

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

2
.nvmrc
View File

@@ -1 +1 @@
v14.18.0
v18.13.0

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120
}

BIN
assets/images/cli-demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
assets/images/landing-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -23,25 +23,7 @@ You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) an
### Lets start coding
```bash
# clone and cd into bruno
# use Node 14.x, Npm 8.x
# Install deps (note that we use npm workspaces)
npm i
# run next app
npm run dev:web
# run electron app
# neededonly if you want to test changes related to electron app
# please note that both web and electron use the same code
# if it works in web, then it should also work in electron
npm run dev:electron
# open in browser
open http://localhost:3000
```
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
### Raising Pull Request

View File

@@ -1,27 +1,53 @@
## development
## 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.
### Dependencies
* NodeJS v18
### Local Development
```bash
# use nodejs 18 version
nvm use
# install deps
npm i
npm i --legacy-peer-deps
# run next app
npm run dev --workspace=packages/bruno-app
# build graphql docs
# note: you can for now ignore the error thrown while building the graphql docs
npm run build:graphql-docs
# run electron app
npm run dev --workspace=packages/bruno-electron
# build bruno query
npm run build:bruno-query
# build next app
npm run build --workspace=packages/bruno-app
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
## fix
### Troubleshooting
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
# testing
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@@ -4,27 +4,37 @@
"workspaces": [
"packages/bruno-app",
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"husky": "^8.0.3",
"jest": "^29.2.0",
"randomstring": "^1.2.2"
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"ts-jest": "^29.0.5"
},
"scripts": {
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
"build:electron": "./scripts/build-electron.sh",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report"
"test:report": "npx playwright show-report",
"prepare": "husky install"
},
"overrides": {
"rollup": "3.2.5"

View File

@@ -3,5 +3,5 @@
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 180
"printWidth": 120
}

View File

@@ -1,8 +1,9 @@
module.exports = {
reactStrictMode: true,
reactStrictMode: false,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT
PLAYWRIGHT: process.env.PLAYWRIGHT,
ENV: process.env.ENV
},
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module

View File

@@ -1,11 +1,13 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "cross-env ENV=dev next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint",
"test": "jest",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
@@ -15,8 +17,8 @@
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/schema": "0.2.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.3.1",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -26,7 +28,7 @@
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"graphiql": "^1.5.9",
"graphql": "^16.2.0",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
@@ -34,19 +36,21 @@
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.1",
"next": "12.3.3",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"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-tabs": "^3.2.3",
"reckonjs": "^0.1.2",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"
@@ -58,6 +62,7 @@
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",

View File

@@ -1,23 +0,0 @@
import { get, post, put } from './base';
// not used. kept as a placeholder for reference while implementing license key stuff
const AuthApi = {
whoami: () => get('auth/v1/user/whoami'),
signup: (params) => post('auth/v1/user/signup', params),
login: (params) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window.require('electron');
ipcRenderer
.invoke('bruno-account-request', {
data: params,
method: 'POST',
url: `${process.env.NEXT_PUBLIC_BRUNO_SERVER_API}/auth/v1/user/login`
})
.then(resolve)
.catch(reject);
});
}
};
export default AuthApi;

View File

@@ -1,30 +0,0 @@
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API
});
apiClient.interceptors.request.use(
(config) => {
const headers = {
'Content-Type': 'application/json'
};
return {
...config,
headers: headers
};
},
(error) => Promise.reject(error)
);
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error.response ? error.response.data : error);
}
);
const { get, post, put, delete: destroy } = apiClient;
export { get, post, put, destroy };

View File

@@ -14,7 +14,11 @@ const Bruno = ({ width }) => {
stroke="none"
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855" />
<polygon
fill="#3F3F3F"
stroke="none"
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
/>
</g>
<g id="hair" />
<g id="skin" />
@@ -84,8 +88,27 @@ const Bruno = ({ width }) => {
strokeWidth="2"
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632" />
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" />
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
/>
<line
x1="36.2078"
x2="36.2078"
y1="47.3393"
y2="44.3093"
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
</g>
</svg>
);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Modal from 'components/Modal/index';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const BrunoSupport = ({ onClose }) => {
@@ -8,6 +8,12 @@ const BrunoSupport = ({ onClose }) => {
<StyledWrapper>
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
<div className="collection-options">
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} />

View File

@@ -6,13 +6,26 @@ const StyledWrapper = styled.div`
border: solid 1px ${(props) => props.theme.codemirror.border};
}
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #d2d7db;
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
@@ -20,13 +33,20 @@ const StyledWrapper = styled.div`
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom{
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
}
.cm-variable-valid {
color: green;
}
.cm-variable-invalid {
color: red;
}
`;
export default StyledWrapper;

View File

@@ -6,6 +6,9 @@
*/
import React from 'react';
import isEqual from 'lodash/isEqual';
import { getEnvironmentVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
let CodeMirror;
@@ -15,7 +18,7 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
export default class QueryEditor extends React.Component {
export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
@@ -23,6 +26,7 @@ export default class QueryEditor extends React.Component {
// editor is updated, which can later be used to protect the editor from
// unnecessary updates during the update lifecycle.
this.cachedValue = props.value || '';
this.variables = {};
}
componentDidMount() {
@@ -39,6 +43,7 @@ export default class QueryEditor extends React.Component {
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-Enter': () => {
@@ -70,6 +75,7 @@ export default class QueryEditor extends React.Component {
}));
if (editor) {
editor.on('change', this._onEdit);
this.addOverlay();
}
}
@@ -88,7 +94,13 @@ export default class QueryEditor extends React.Component {
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
this.editor.setOption('mode', this.props.mode);
}
if (this.editor) {
let variables = getEnvironmentVariables(this.props.collection);
if (!isEqual(variables, this.variables)) {
this.addOverlay();
}
}
if (this.props.theme !== prevProps.theme && this.editor) {
@@ -116,6 +128,15 @@ export default class QueryEditor extends React.Component {
);
}
addOverlay = () => {
const mode = this.props.mode || 'application/ld+json';
let variables = getEnvironmentVariables(this.props.collection);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode);
this.editor.setOption('mode', 'brunovariables');
};
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();

View File

@@ -5,7 +5,16 @@ import StyledWrapper from './StyledWrapper';
const Dropdown = ({ icon, children, onCreate, placement }) => {
return (
<StyledWrapper className="dropdown">
<Tippy content={children} placement={placement || 'bottom-end'} animation={false} arrow={false} onCreate={onCreate} interactive={true} trigger="click" appendTo="parent">
<Tippy
content={children}
placement={placement || 'bottom-end'}
animation={false}
arrow={false}
onCreate={onCreate}
interactive={true}
trigger="click"
appendTo="parent"
>
{icon}
</Tippy>
</StyledWrapper>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.current-enviroment {
background-color: ${(props) => props.theme.sidebar.workspace.bg};
background-color: ${(props) => props.theme.sidebar.badge.bg};
border-radius: 15px;
.caret {

View File

@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
@@ -35,7 +35,7 @@ const EnvironmentSelector = ({ collection }) => {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occured while selecting the environment'));
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
};
return (
@@ -63,13 +63,14 @@ const EnvironmentSelector = ({ collection }) => {
onSelect(null);
}}
>
<span>No Environment</span>
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Settings</span>
<span>Configure</span>
</div>
</Dropdown>
</div>

View File

@@ -16,7 +16,10 @@ const CreateEnvironment = ({ collection, onClose }) => {
name: ''
},
validationSchema: Yup.object({
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(addEnvironment(values.name, collection.uid))
@@ -40,7 +43,13 @@ const CreateEnvironment = ({ collection, onClose }) => {
return (
<Portal>
<Modal size="sm" title={'Create Environment'} confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<Modal
size="sm"
title={'Create Environment'}
confirmText="Create"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
@@ -59,7 +68,9 @@ const CreateEnvironment = ({ collection, onClose }) => {
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>

View File

@@ -20,7 +20,13 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
return (
<Portal>
<StyledWrapper>
<Modal size="sm" title={'Delete Environment'} confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
<Modal
size="sm"
title={'Delete Environment'}
confirmText="Delete"
handleConfirm={onConfirm}
handleCancel={onClose}
>
Are you sure you want to delete <span className="font-semibold">{environment.name}</span> ?
</Modal>
</StyledWrapper>

View File

@@ -5,18 +5,30 @@ 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 {
color: ${(props) => props.theme.table.thead.color};;
color: ${(props) => props.theme.table.thead.color};
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;
@@ -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,24 +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>
);
@@ -119,7 +137,12 @@ const EnvironmentVariables = ({ environment, collection }) => {
</div>
<div>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" disabled={!hasChanges} onClick={saveChanges}>
<button
type="submit"
className="submit btn btn-md btn-secondary mt-2"
disabled={!hasChanges}
onClick={saveChanges}
>
Save
</button>
</div>

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

@@ -7,12 +7,19 @@ import DeleteEnvironment from '../../DeleteEnvironment';
const EnvironmentDetails = ({ environment, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
console.log(environment);
return (
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />}
{openEditModal && (
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />
)}
{openDeleteModal && (
<DeleteEnvironment
onClose={() => setOpenDeleteModal(false)}
environment={environment}
collection={collection}
/>
)}
<div className="flex">
<div className="flex flex-grow items-center">
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />

View File

@@ -1,4 +1,6 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react';
import { findEnvironmentInCollection } from 'utils/collections';
import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment/index';
import StyledWrapper from './StyledWrapper';
@@ -8,9 +10,36 @@ const EnvironmentList = ({ collection }) => {
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
const envUids = environments ? environments.map((env) => env.uid) : [];
const prevEnvUids = usePrevious(envUids);
useEffect(() => {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}, [environments]);
if (selectedEnvironment) {
return;
}
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if (environment) {
setSelectedEnvironment(environment);
} else {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}
}, [collection, environments, selectedEnvironment]);
useEffect(() => {
// check env add
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
if (newEnv) {
setSelectedEnvironment(newEnv);
}
}
// check env delete
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}
}, [envUids, environments, prevEnvUids]);
if (!selectedEnvironment) {
return null;
@@ -25,7 +54,11 @@ const EnvironmentList = ({ collection }) => {
{environments &&
environments.length &&
environments.map((env) => (
<div key={env.uid} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} onClick={() => setSelectedEnvironment(env)}>
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span>
</div>
))}

View File

@@ -16,7 +16,10 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
name: environment.name
},
validationSchema: Yup.object({
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
@@ -40,7 +43,13 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
return (
<Portal>
<Modal size="sm" title={'Rename Environment'} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
<Modal
size="sm"
title={'Rename Environment'}
confirmText="Rename"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
@@ -59,7 +68,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>

View File

@@ -11,11 +11,21 @@ const EnvironmentSettings = ({ collection, onClose }) => {
if (!environments || !environments.length) {
return (
<StyledWrapper>
<Modal size="md" title="Environments" confirmText={'Close'} handleConfirm={onClose} handleCancel={onClose} hideCancel={true}>
<Modal
size="md"
title="Environments"
confirmText={'Close'}
handleConfirm={onClose}
handleCancel={onClose}
hideCancel={true}
>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
<div className="text-center">
<p>No environments found!</p>
<button className="btn-create-environment text-link pr-2 py-3 mt-2 select-none" onClick={() => setOpenCreateModal(true)}>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
+ <span>Create Environment</span>
</button>
</div>

View File

@@ -1,17 +1,12 @@
import React from 'react';
const SendIcon = ({color, width}) => {
const SendIcon = ({ color, width }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 48 48"
>
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z"/>
<path d="M0 0h48v48h-48z" fill="none"/>
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} viewBox="0 0 48 48">
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z" />
<path d="M0 0h48v48h-48z" fill="none" />
</svg>
);
}
};
export default SendIcon;

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;
@@ -100,7 +100,7 @@ const Wrapper = styled.div`
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out .1s;
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};

View File

@@ -14,7 +14,15 @@ const ModalHeader = ({ title, handleCancel }) => (
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel, hideFooter }) => {
const ModalFooter = ({
confirmText,
cancelText,
handleSubmit,
handleCancel,
confirmDisabled,
hideCancel,
hideFooter
}) => {
confirmText = confirmText || 'Save';
cancelText = cancelText || 'Cancel';
@@ -30,7 +38,12 @@ const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, conf
</button>
</span>
<span>
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit}>
<button
type="submit"
className="submit btn btn-md btn-secondary"
disabled={confirmDisabled}
onClick={handleSubmit}
>
{confirmText}
</button>
</span>
@@ -38,7 +51,18 @@ const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, conf
);
};
const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfirm, children, confirmDisabled, hideCancel, hideFooter }) => {
const Modal = ({
size,
title,
confirmText,
cancelText,
handleCancel,
handleConfirm,
children,
confirmDisabled,
hideCancel,
hideFooter
}) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
const escKeyCode = 27;
@@ -64,7 +88,7 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
if (isClosing) {
classes += ' modal--animate-out';
}
if(hideFooter) {
if (hideFooter) {
classes += ' modal-footer-none';
}
return (

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: ${(props) => props.theme.text};
`;
export default StyledWrapper;

View File

@@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { usePreferences } from 'providers/Preferences';
import StyledWrapper from './StyledWrapper';
const General = () => {
const { preferences, setPreferences } = usePreferences();
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
const handleCheckboxChange = () => {
const updatedPreferences = {
...preferences,
request: {
...preferences.request,
sslVerification: !sslVerification
}
};
setPreferences(updatedPreferences)
.then(() => {
setSslVerification(!sslVerification);
})
.catch((err) => {
console.error(err);
});
};
return (
<StyledWrapper>
<div className="flex items-center mt-2">
<input type="checkbox" checked={sslVerification} onChange={handleCheckboxChange} className="mr-3 mousetrap" />
SSL Certificate Verification
</div>
</StyledWrapper>
);
};
export default General;

View File

@@ -0,0 +1,36 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
margin-top: -0.5rem;
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
section.tab-panel {
min-height: 300px;
}
`;
export default StyledWrapper;

View File

@@ -1,8 +1,8 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: var(--color-text);
.collection-options {
color: ${(props) => props.theme.text};
.rows {
svg {
position: relative;
top: -1px;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const Support = () => {
return (
<StyledWrapper>
<div className="rows">
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
</a>
</div>
<div className="mt-2">
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">Discord</span>
</a>
</div>
<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>
</a>
</div>
<div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span>
</a>
</div>
</div>
</StyledWrapper>
);
};
export default Support;

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: var(--color-text);
`;
export default StyledWrapper;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
const Theme = () => {
const { storedTheme, setStoredTheme } = useTheme();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
theme: storedTheme
},
validationSchema: Yup.object({
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
}),
onSubmit: (values) => {
setStoredTheme(values.theme);
}
});
return (
<StyledWrapper>
<div className="bruno-form">
<div className="flex items-center mt-2">
<input
id="light-theme"
className="cursor-pointer"
type="radio"
name="theme"
onChange={(e) => {
formik.handleChange(e);
formik.handleSubmit();
}}
value="light"
checked={formik.values.theme === 'light'}
/>
<label htmlFor="light-theme" className="ml-1 cursor-pointer select-none">
Light
</label>
<input
id="dark-theme"
className="ml-4 cursor-pointer"
type="radio"
name="theme"
onChange={(e) => {
formik.handleChange(e);
formik.handleSubmit();
}}
value="dark"
checked={formik.values.theme === 'dark'}
/>
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
Dark
</label>
</div>
</div>
</StyledWrapper>
);
};
export default Theme;

View File

@@ -0,0 +1,54 @@
import Modal from 'components/Modal/index';
import classnames from 'classnames';
import React, { useState } from 'react';
import Support from './Support';
import General from './General';
import Theme from './Theme';
import StyledWrapper from './StyledWrapper';
const Preferences = ({ onClose }) => {
const [tab, setTab] = useState('general');
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === tab
});
};
const getTabPanel = (tab) => {
switch (tab) {
case 'general': {
return <General />;
}
case 'theme': {
return <Theme />;
}
case 'support': {
return <Support />;
}
}
};
return (
<StyledWrapper>
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
<div className="flex items-center px-2 tabs" role="tablist">
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
General
</div>
<div className={getTabClassname('theme')} role="tab" onClick={() => setTab('theme')}>
Theme
</div>
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
Support
</div>
</div>
<section className="flex flex-grow px-2 mt-4 tab-panel">{getTabPanel(tab)}</section>
</Modal>
</StyledWrapper>
);
};
export default Preferences;

View File

@@ -0,0 +1,90 @@
import React from 'react';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const AssertionOperator = ({ operator, onChange }) => {
const operators = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'in',
'notIn',
'contains',
'notContains',
'length',
'matches',
'notMatches',
'startsWith',
'endsWith',
'between',
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
const handleChange = (e) => {
onChange(e.target.value);
};
const getLabel = (operator) => {
switch (operator) {
case 'eq':
return 'equals';
case 'neq':
return 'notEquals';
default:
return operator;
}
};
return (
<select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => (
<option key={operator} value={operator}>
{getLabel(operator)}
</option>
))}
</select>
);
};
export default AssertionOperator;

View File

@@ -0,0 +1,212 @@
import React from 'react';
import { IconTrash } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import AssertionOperator from '../AssertionOperator';
import { useTheme } from 'providers/Theme';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const parseAssertionOperator = (str = '') => {
if (!str || typeof str !== 'string' || !str.length) {
return {
operator: 'eq',
value: str
};
}
const operators = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'in',
'notIn',
'contains',
'notContains',
'length',
'matches',
'notMatches',
'startsWith',
'endsWith',
'between',
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
const unaryOperators = [
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
const [operator, ...rest] = str.trim().split(' ');
const value = rest.join(' ');
if (unaryOperators.includes(operator)) {
return {
operator,
value: ''
};
}
if (operators.includes(operator)) {
return {
operator,
value
};
}
return {
operator: 'eq',
value: str
};
};
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
return unaryOperators.includes(operator);
};
const AssertionRow = ({
item,
collection,
assertion,
handleAssertionChange,
handleRemoveAssertion,
onSave,
handleRun
}) => {
const { storedTheme } = useTheme();
const { operator, value } = parseAssertionOperator(assertion.value);
return (
<tr key={assertion.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<td>
<AssertionOperator
operator={operator}
onChange={(op) =>
handleAssertionChange(
{
target: {
value: `${op} ${value}`
}
},
assertion,
'value'
)
}
/>
</td>
<td>
{!isUnaryOperator(operator) ? (
<SingleLineEditor
value={value}
theme={storedTheme}
readOnly={true}
onSave={onSave}
onChange={(newValue) =>
handleAssertionChange(
{
target: {
value: newValue
}
},
assertion,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
) : (
<input type="text" className="cursor-default" disabled />
)}
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={assertion.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
/>
<button onClick={() => handleRemoveAssertion(assertion)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
};
export default AssertionRow;

View File

@@ -0,0 +1,60 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(4) {
width: 70px;
}
select {
background-color: transparent;
}
}
}
.btn-add-assertion {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,96 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { useDispatch } from 'react-redux';
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import AssertionRow from './AssertionRow';
import StyledWrapper from './StyledWrapper';
const Assertions = ({ item, collection }) => {
const dispatch = useDispatch();
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
const handleAddAssertion = () => {
dispatch(
addAssertion({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleAssertionChange = (e, _assertion, type) => {
const assertion = cloneDeep(_assertion);
switch (type) {
case 'name': {
assertion.name = e.target.value;
break;
}
case 'value': {
assertion.value = e.target.value;
break;
}
case 'enabled': {
assertion.enabled = e.target.checked;
break;
}
}
dispatch(
updateAssertion({
assertion: assertion,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveAssertion = (assertion) => {
dispatch(
deleteAssertion({
assertUid: assertion.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Expr</td>
<td>Operator</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{assertions && assertions.length
? assertions.map((assertion) => {
return (
<AssertionRow
key={assertion.uid}
assertion={assertion}
item={item}
collection={collection}
handleAssertionChange={handleAssertionChange}
handleRemoveAssertion={handleRemoveAssertion}
onSave={onSave}
handleRun={handleRun}
/>
);
})
: null}
</tbody>
</table>
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
+ Add Assertion
</button>
</StyledWrapper>
);
};
export default Assertions;

View File

@@ -5,6 +5,7 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@@ -3,11 +3,19 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import {
addFormUrlEncodedParam,
updateFormUrlEncodedParam,
deleteFormUrlEncodedParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
const addParam = () => {
@@ -19,6 +27,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
@@ -30,10 +40,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
@@ -65,7 +71,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
@@ -87,32 +92,33 @@ const FormUrlEncodedParams = ({ item, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>

View File

@@ -6,10 +6,16 @@ import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryEditor from 'components/RequestPane/QueryEditor';
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import { useTheme } from 'providers/Theme';
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findEnvironmentInCollection } from 'utils/collections';
import useGraphqlSchema from './useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
@@ -18,26 +24,24 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
const variables = item.draft
? get(item, 'draft.request.body.graphql.variables')
: get(item, 'request.body.graphql.variables');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const {
storedTheme
} = useTheme();
let {
schema,
loadSchema,
isLoading: isSchemaLoading,
error: schemaError
} = useGraphqlSchema(url);
const { storedTheme } = useTheme();
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let { schema, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment);
const loadGqlSchema = () => {
if(!isSchemaLoading) {
if (!isSchemaLoading) {
loadSchema();
}
};
useEffect(() => {
if(onSchemaLoad) {
if (onSchemaLoad) {
onSchemaLoad(schema);
}
}, [schema]);
@@ -66,20 +70,38 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const getTabPanel = (tab) => {
switch (tab) {
case 'query': {
return <QueryEditor
theme={storedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}
value={query}
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
/>;
return (
<QueryEditor
collection={collection}
theme={storedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}
value={query}
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
/>
);
}
case 'variables': {
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
}
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
case 'assert': {
return <Assertions item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;
}
case 'tests': {
return <Tests item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
@@ -107,23 +129,34 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
Query
</div>
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
Variables
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
{isSchemaLoading ? (
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
) : null}
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
<div className="flex items-center cursor-pointer hover:underline" onClick={loadGqlSchema}>
{isSchemaLoading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5} /> : null}
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5} /> : null}
<span className="ml-1">Schema</span>
</div>
<div
className='flex items-center cursor-pointer hover:underline ml-2'
onClick={toggleDocs}
>
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
<div className="flex items-center cursor-pointer hover:underline ml-2" onClick={toggleDocs}>
<IconBook size={18} strokeWidth={1.5} />
<span className="ml-1">Docs</span>
</div>
</div>
</div>

View File

@@ -1,34 +1,19 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
import { buildClientSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const fetchSchema = (endpoint) => {
const introspectionQuery = getIntrospectionQuery();
const queryParams = {
query: introspectionQuery
};
return fetch(endpoint, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(queryParams)
});
}
const useGraphqlSchema = (endpoint) => {
const useGraphqlSchema = (endpoint, environment) => {
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [schema, setSchema] = useState(() => {
try {
const saved = localStorage.getItem(localStorageKey);
if(!saved) {
if (!saved) {
return null;
}
return buildClientSchema(JSON.parse(saved));
@@ -40,14 +25,14 @@ const useGraphqlSchema = (endpoint) => {
const loadSchema = () => {
setIsLoading(true);
fetchSchema(endpoint)
.then((res) => res.json())
fetchGqlSchema(endpoint, environment)
.then((res) => res.data)
.then((s) => {
if (s && s.data) {
setSchema(buildClientSchema(s.data));
setIsLoading(false);
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
toast.success('Graphql Schema loaded successfully');
toast.success('GraphQL Schema loaded successfully');
} else {
return Promise.reject(new Error('An error occurred while introspecting schema'));
}
@@ -55,7 +40,7 @@ const useGraphqlSchema = (endpoint) => {
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error('Error occured while loading Graphql Schema');
toast.error('Error occured while loading GraphQL Schema');
});
};

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestGraphqlVariables({
variables: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
value={variables || ''}
theme={storedTheme}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default GraphQLVariables;

View File

@@ -7,6 +7,10 @@ import QueryParams from 'components/RequestPane/QueryParams';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
@@ -34,6 +38,18 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
case 'assert': {
return <Assertions item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;
}
case 'tests': {
return <Tests item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
@@ -59,7 +75,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Params
Query
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
@@ -67,6 +83,18 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
{/* Moved to post mvp */}
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
{focusedTab.requestPaneTab === 'body' ? (
@@ -75,7 +103,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
) : null}
</div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
{getTabPanel(focusedTab.requestPaneTab)}
</section>
</StyledWrapper>
);
};

View File

@@ -5,6 +5,7 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@@ -3,11 +3,19 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
const addParam = () => {
@@ -19,6 +27,8 @@ const MultipartFormParams = ({ item, collection }) => {
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
@@ -30,10 +40,6 @@ const MultipartFormParams = ({ item, collection }) => {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
@@ -65,7 +71,6 @@ const MultipartFormParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
@@ -87,32 +92,33 @@ const MultipartFormParams = ({ item, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>

View File

@@ -14,7 +14,20 @@ const StyledWrapper = styled.div`
// Todo: dark mode temporary fix
// Clean this
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
@@ -22,13 +35,20 @@ const StyledWrapper = styled.div`
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom{
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
}
.cm-variable-valid {
color: green;
}
.cm-variable-invalid {
color: red;
}
`;
export default StyledWrapper;

View File

@@ -6,7 +6,10 @@
*/
import React from 'react';
import isEqual from 'lodash/isEqual';
import MD from 'markdown-it';
import { getAllVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import onHasCompletion from './onHasCompletion';
@@ -29,6 +32,7 @@ export default class QueryEditor extends React.Component {
// editor is updated, which can later be used to protect the editor from
// unnecessary updates during the update lifecycle.
this.cachedValue = props.value || '';
this.variables = {};
}
componentDidMount() {
@@ -37,12 +41,17 @@ export default class QueryEditor extends React.Component {
lineNumbers: true,
tabSize: 2,
mode: 'graphql',
// mode: 'brunovariables',
brunoVarInfo: {
variables: getAllVariables(this.props.collection)
},
theme: this.props.editorTheme || 'graphiql',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
scrollbarStyle: 'overlay',
readOnly: this.props.readOnly ? 'nocursor' : false,
foldGutter: {
minFoldSize: 4
@@ -129,6 +138,7 @@ export default class QueryEditor extends React.Component {
editor.on('hasCompletion', this._onHasCompletion);
editor.on('beforeChange', this._onBeforeChange);
}
this.addOverlay();
}
componentDidUpdate(prevProps) {
@@ -151,6 +161,11 @@ export default class QueryEditor extends React.Component {
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
let variables = getAllVariables(this.props.collection);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
this.addOverlay();
}
this.ignoreChangeEvent = false;
}
@@ -163,6 +178,15 @@ export default class QueryEditor extends React.Component {
}
}
// Todo: Overlay is messing up with schema hint
// Fix this
addOverlay = () => {
// let variables = getAllVariables(this.props.collection);
// this.variables = variables;
// defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
// this.editor.setOption('mode', 'brunovariables');
};
render() {
return (
<StyledWrapper

View File

@@ -59,7 +59,10 @@ export default function onHasCompletion(_cm, data, onHintInformationRender) {
const description = ctx.description ? md.render(ctx.description) : 'Self descriptive.';
const type = ctx.type ? '<span className="infoType">' + renderType(ctx.type) + '</span>' : '';
information.innerHTML = '<div className="content">' + (description.slice(0, 3) === '<p>' ? '<p>' + type + description.slice(3) : type + description) + '</div>';
information.innerHTML =
'<div className="content">' +
(description.slice(0, 3) === '<p>' ? '<p>' + type + description.slice(3) : type + description) +
'</div>';
if (ctx && deprecation && ctx.deprecationReason) {
const reason = ctx.deprecationReason ? md.render(ctx.deprecationReason) : '';

View File

@@ -5,6 +5,7 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
@@ -12,12 +13,20 @@ const Wrapper = styled.div`
}
thead {
color: ${(props) => props.theme.table.thead.color};;
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}

View File

@@ -3,12 +3,16 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const handleAddParam = () => {
@@ -20,6 +24,8 @@ const QueryParams = ({ item, collection }) => {
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
@@ -32,10 +38,6 @@ const QueryParams = ({ item, collection }) => {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
@@ -66,9 +68,8 @@ const QueryParams = ({ item, collection }) => {
<table>
<thead>
<tr>
<td>Key</td>
<td>Name</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
@@ -90,32 +91,33 @@ const QueryParams = ({ item, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>

View File

@@ -10,7 +10,9 @@ const HttpMethodSelector = ({ method, onMethodSelect }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
<div className="flex-grow font-medium" id="create-new-request-method">{method}</div>
<div className="flex-grow font-medium" id="create-new-request-method">
{method}
</div>
<div>
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>

View File

@@ -1,18 +1,28 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({ item, collection, handleRun }) => {
const { theme } = useTheme();
const { theme, storedTheme } = useTheme();
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
useEffect(() => {
const el = document.querySelector('.method-selector-container');
setMethodSelectorWidth(el.offsetWidth);
}, [method]);
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onUrlChange = (value) => {
dispatch(
requestUrlChanged({
@@ -38,19 +48,24 @@ const QueryUrl = ({ item, collection, handleRun }) => {
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full mousetrap"
type="text"
<div
className="flex items-center flex-grow input-container h-full"
style={{
color: 'yellow',
width: `calc(100% - ${methodSelectorWidth}px)`,
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
}}
>
<SingleLineEditor
value={url}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={(event) => onUrlChange(event.target.value)}
onSave={onSave}
theme={storedTheme}
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
collection={collection}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
</div>
</div>
</StyledWrapper>

View File

@@ -52,7 +52,7 @@ const RequestBodyMode = ({ item, collection }) => {
onModeChange('formUrlEncoded');
}}
>
Form Url Encoded
Form URL Encoded
</div>
<div className="label-item font-medium">Raw</div>
<div

View File

@@ -13,9 +13,7 @@ const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
const {
storedTheme
} = useTheme();
const { storedTheme } = useTheme();
const onEdit = (value) => {
dispatch(
@@ -45,7 +43,15 @@ const RequestBody = ({ item, collection }) => {
return (
<StyledWrapper className="w-full">
<CodeEditor theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
<CodeEditor
collection={collection}
theme={storedTheme}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}
onSave={onSave}
mode={codeMirrorMode[bodyMode]}
/>
</StyledWrapper>
);
}

View File

@@ -5,6 +5,7 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}

View File

@@ -3,11 +3,15 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
@@ -19,6 +23,8 @@ const RequestHeaders = ({ item, collection }) => {
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
@@ -30,10 +36,6 @@ const RequestHeaders = ({ item, collection }) => {
header.value = e.target.value;
break;
}
case 'description': {
header.description = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
@@ -63,9 +65,8 @@ const RequestHeaders = ({ item, collection }) => {
<table>
<thead>
<tr>
<td>Key</td>
<td>Name</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
@@ -87,32 +88,33 @@ const RequestHeaders = ({ item, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={header.enabled} className="mr-3 mousetrap" onChange={(e) => handleHeaderValueChange(e, header, 'enabled')} />
<input
type="checkbox"
checked={header.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>

View File

@@ -28,7 +28,14 @@ const SaveRequest = ({ items, onClose }) => {
return (
<StyledWrapper>
<Modal size="md" title="Save Request" confirmText="Save" cancelText="Cancel" handleCancel={onClose} handleConfirm={onClose}>
<Modal
size="md"
title="Save Request"
confirmText="Save"
cancelText="Cancel"
handleCancel={onClose}
handleConfirm={onClose}
>
<p className="mb-2">Select a folder to save request:</p>
<div className="folder-list">
{showFolders && showFolders.length

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: inherit;
}
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Script = ({ item, collection }) => {
const dispatch = useDispatch();
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
const { storedTheme } = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateResponseScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</div>
</StyledWrapper>
);
};
export default Script;

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
const { storedTheme } = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestTests({
tests: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default Tests;

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import StyledWrapper from './StyledWrapper';
const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const handleAddVar = () => {
dispatch(
addVar({
type: varType,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
_var.name = e.target.value;
break;
}
case 'value': {
_var.value = e.target.value;
break;
}
case 'enabled': {
_var.enabled = e.target.checked;
break;
}
}
dispatch(
updateVar({
type: varType,
var: _var,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveVar = (_var) => {
dispatch(
deleteVar({
type: varType,
varUid: _var.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{varType === 'request' ? (
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
? vars.map((_var, index) => {
return (
<tr key={_var.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={_var.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, _var, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleVarChange(
{
target: {
value: newValue
}
},
_var,
'value'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={_var.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
+ Add
</button>
</StyledWrapper>
);
};
export default VarsTable;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
const Vars = ({ item, collection }) => {
const dispatch = useDispatch();
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
const { storedTheme } = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateResponseScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
</div>
</StyledWrapper>
);
};
export default Vars;

View File

@@ -1,9 +1,10 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const RequestNotFound = ({ itemUid }) => {
const dispatch = useDispatch();
const [showErrorMessage, setShowErrorMessage] = useState(false);
const closeTab = () => {
dispatch(
@@ -13,11 +14,27 @@ const RequestNotFound = ({ itemUid }) => {
);
};
useEffect(() => {
setTimeout(() => {
setShowErrorMessage(true);
}, 300);
}, []);
// add a delay component in react that shows a loading spinner
// and then shows the error message after a delay
// this will prevent the error message from flashing on the screen
if (!showErrorMessage) {
return null;
}
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
<div>Request no longer exists.</div>
<div className="mt-2">This can happen when the yml file associated with this request was deleted on your filesystem.</div>
<div className="mt-2">
This can happen when the .bru file associated with this request was deleted on your filesystem.
</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab

View File

@@ -12,6 +12,8 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
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 { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
@@ -32,7 +34,9 @@ const RequestTabPanel = () => {
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
const [leftPaneWidth, setLeftPaneWidth] = useState(
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
const [dragging, setDragging] = useState(false);
@@ -44,10 +48,10 @@ const RequestTabPanel = () => {
const onSchemaLoad = (schema) => setSchema(schema);
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
const handleGqlClickReference = (reference) => {
if(docExplorerRef.current) {
if (docExplorerRef.current) {
docExplorerRef.current.showDocForReference(reference);
}
if(!showGqlDocs) {
if (!showGqlDocs) {
setShowGqlDocs(true);
}
};
@@ -65,10 +69,13 @@ const RequestTabPanel = () => {
if (dragging) {
e.preventDefault();
let leftPaneXPosition = e.clientX + 2;
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
if (
leftPaneXPosition < asideWidth + DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH ||
leftPaneXPosition > screenWidth - MIN_RIGHT_PANE_WIDTH
) {
return;
}
setLeftPaneWidth(leftPaneXPosition- asideWidth);
setLeftPaneWidth(leftPaneXPosition - asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
}
};
@@ -112,6 +119,15 @@ const RequestTabPanel = () => {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const showRunner = collection.showRunner;
if (showRunner) {
return <RunnerResults collection={collection} />;
}
if (focusedTab.type === 'variables') {
return <VariablesEditor collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
@@ -132,7 +148,13 @@ const RequestTabPanel = () => {
</div>
<section className="main flex flex-grow pb-4 relative">
<section className="request-pane">
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
<div
className="px-4"
style={{
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`,
height: `calc(100% - ${DEFAULT_PADDING}px)`
}}
>
{item.type === 'graphql-request' ? (
<GraphQLRequestPane
item={item}
@@ -144,7 +166,9 @@ const RequestTabPanel = () => {
/>
) : null}
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} /> : null}
{item.type === 'http-request' ? (
<HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} />
) : null}
</div>
</section>
@@ -159,19 +183,15 @@ const RequestTabPanel = () => {
{item.type === 'graphql-request' ? (
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
<DocExplorer schema={schema} ref={(r) => docExplorerRef.current = r}>
<button
className='mr-2'
onClick={toggleDocs}
aria-label="Close Documentation Explorer"
>
<DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>
<button className="mr-2" onClick={toggleDocs} aria-label="Close Documentation Explorer">
{'\u2715'}
</button>
</DocExplorer>
</div>
): null}
) : null}
</StyledWrapper>
);
};
export default RequestTabPanel;
export default RequestTabPanel;

View File

@@ -1,9 +1,33 @@
import React from 'react';
import { IconFiles } from '@tabler/icons';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
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 }) => {
const dispatch = useDispatch();
const handleRun = () => {
dispatch(
toggleRunnerView({
collectionUid: collection.uid
})
);
};
const viewVariables = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'variables'
})
);
};
return (
<StyledWrapper>
<div className="flex items-center p-2">
@@ -12,6 +36,12 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</span>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@@ -0,0 +1,42 @@
import React, { useState, useEffect } from 'react';
import { IconAlertTriangle } from '@tabler/icons';
const RequestTabNotFound = ({ handleCloseClick }) => {
const [showErrorMessage, setShowErrorMessage] = useState(false);
// add a delay component in react that shows a loading spinner
// and then shows the error message after a delay
// this will prevent the error message from flashing on the screen
useEffect(() => {
setTimeout(() => {
setShowErrorMessage(true);
}, 300);
}, []);
if (!showErrorMessage) {
return null;
}
return (
<>
<div className="flex items-center tab-label pl-2">
{showErrorMessage ? (
<>
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</>
) : null}
</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 RequestTabNotFound;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { IconVariable } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, text }) => {
return (
<>
<div className="flex items-center tab-label pl-2">
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">{text}</span>
</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

@@ -4,7 +4,8 @@ import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { IconAlertTriangle } from '@tabler/icons';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
@@ -56,23 +57,20 @@ const RequestTab = ({ tab, collection }) => {
return color;
};
if (tab.type === 'variables') {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" />
</StyledWrapper>
);
}
const item = findItemInCollection(collection, tab.uid);
if (!item) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</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>
<RequestTabNotFound handleCloseClick={handleCloseClick} />
</StyledWrapper>
);
}
@@ -98,7 +96,15 @@ const RequestTab = ({ tab, collection }) => {
></path>
</svg>
) : (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="16" fill="#cc7b1b" className="has-changes-icon" viewBox="0 0 8 8">
<svg
focusable="false"
xmlns="http://www.w3.org/2000/svg"
width="8"
height="16"
fill="#cc7b1b"
className="has-changes-icon"
viewBox="0 0 8 8"
>
<circle cx="4" cy="4" r="3" />
</svg>
)}

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
import find from 'lodash/find';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { focusTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -76,66 +76,83 @@ const RequestTabs = () => {
});
};
const showRunner = activeCollection && activeCollection.showRunner;
// Todo: Must support ephermal requests
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && <NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />}
{newRequestModalOpen && (
<NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />
)}
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection} />
<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} />
{!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>
) : 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>
</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}>
<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">
<IconChevronRight size={18} strokeWidth={1.5} />
<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>
) : 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>
{/* 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}
</>
) : null}
</StyledWrapper>

View File

@@ -7,9 +7,6 @@ const NetworkError = ({ onClose }) => {
<div className="flex items-start">
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-red-800">Network Error</p>
<p className="mt-2 text-xs text-gray-500">
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS. If not, please use the chrome extension or the desktop app
</p>
</div>
</div>
</div>

View File

@@ -1,17 +1,33 @@
import React from 'react';
import CodeEditor from 'components/CodeEditor';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const QueryResult = ({ value, width }) => {
const {
storedTheme
} = useTheme();
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
const { storedTheme } = useTheme();
const dispatch = useDispatch();
const onRun = () => {
if (disableRunEventListener) {
return;
}
dispatch(sendRequest(item, collection.uid));
};
return (
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
<div className="h-full">
<CodeEditor theme={storedTheme} value={value || ''} readOnly />
<CodeEditor
collection={collection}
theme={storedTheme}
onRun={onRun}
value={value || ''}
mode={mode}
readOnly
/>
</div>
</StyledWrapper>
);

View File

@@ -3,11 +3,11 @@ import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.75rem;
font-weight: 600;
&.text-ok {
color: ${(props) => props.theme.requestTabPanel.responseOk};
}
&.text-error {
color: ${(props) => props.theme.requestTabPanel.responseError};
}

View File

@@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
}
}
}
.some-tests-failed {
color: ${(props) => props.theme.colors.text.danger} !important;
}
.all-tests-passed {
color: ${(props) => props.theme.colors.text.green} !important;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,17 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.test-success {
color: ${(props) => props.theme.colors.text.green};
}
.test-failure {
color: ${(props) => props.theme.colors.text.danger};
}
.error-message {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const TestResults = ({ results, assertionResults }) => {
results = results || [];
assertionResults = assertionResults || [];
if (!results.length && !assertionResults.length) {
return <div className="px-3">No tests found</div>;
}
const passedTests = results.filter((result) => result.status === 'pass');
const failedTests = results.filter((result) => result.status === 'fail');
const passedAssertions = assertionResults.filter((result) => result.status === 'pass');
const failedAssertions = assertionResults.filter((result) => result.status === 'fail');
return (
<StyledWrapper className="flex flex-col px-3">
<div className="py-2 font-medium test-summary">
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
</div>
<ul className="">
{results.map((result) => (
<li key={result.uid} className="py-1">
{result.status === 'pass' ? (
<span className="test-success">&#x2714;&nbsp; {result.description}</span>
) : (
<>
<span className="test-failure">&#x2718;&nbsp; {result.description}</span>
<br />
<span className="error-message pl-8">{result.error}</span>
</>
)}
</li>
))}
</ul>
<div className="py-2 font-medium test-summary">
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '}
{failedAssertions.length}
</div>
<ul className="">
{assertionResults.map((result) => (
<li key={result.uid} className="py-1">
{result.status === 'pass' ? (
<span className="test-success">
&#x2714;&nbsp; {result.lhsExpr}: {result.rhsExpr}
</span>
) : (
<>
<span className="test-failure">
&#x2718;&nbsp; {result.lhsExpr}: {result.rhsExpr}
</span>
<br />
<span className="error-message pl-8">{result.error}</span>
</>
)}
</li>
))}
</ul>
</StyledWrapper>
);
};
export default TestResults;

View File

@@ -0,0 +1,31 @@
import React from 'react';
const TestResultsLabel = ({ results, assertionResults }) => {
results = results || [];
assertionResults = assertionResults || [];
if (!results.length && !assertionResults.length) {
return 'Tests';
}
const numberOfTests = results.length;
const numberOfFailedTests = results.filter((result) => result.status === 'fail').length;
const numberOfAssertions = assertionResults.length;
const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length;
const totalNumberOfTests = numberOfTests + numberOfAssertions;
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions;
return (
<div className="flex items-center">
<div>Tests</div>
{totalNumberOfFailedTests ? (
<sup className="sups some-tests-failed ml-1 font-medium">{totalNumberOfFailedTests}</sup>
) : (
<sup className="sups all-tests-passed ml-1 font-medium">{totalNumberOfTests}</sup>
)}
</div>
);
};
export default TestResultsLabel;

View File

@@ -0,0 +1,24 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.line {
white-space: pre-line;
word-wrap: break-word;
word-break: break-all;
font-family: Inter, sans-serif !important;
.arrow {
opacity: 0.5;
}
&.request {
color: ${(props) => props.theme.colors.text.green};
}
&.response {
color: ${(props) => props.theme.colors.text.purple};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import { safeStringifyJSON } from 'utils/common';
import StyledWrapper from './StyledWrapper';
const Timeline = ({ request, response }) => {
const requestHeaders = [];
const responseHeaders = response.headers || [];
request = request || {};
response = response || {};
forOwn(request.headers, (value, key) => {
requestHeaders.push({
name: key,
value
});
});
let requestData = safeStringifyJSON(request.data);
return (
<StyledWrapper className="px-3 pb-4 w-full">
<div>
<pre className="line request font-bold">
<span className="arrow">{'>'}</span> {request.method} {request.url}
</pre>
{requestHeaders.map((h) => {
return (
<pre className="line request" key={h.name}>
<span className="arrow">{'>'}</span> {h.name}: {h.value}
</pre>
);
})}
{requestData ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data {requestData}
</pre>
) : null}
</div>
<div className="mt-4">
<pre className="line response font-bold">
<span className="arrow">{'<'}</span> {response.status} {response.statusText}
</pre>
{responseHeaders.map((h) => {
return (
<pre className="line response" key={h[0]}>
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
</pre>
);
})}
</div>
</StyledWrapper>
);
};
export default Timeline;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { safeStringifyJSON } from 'utils/common';
import { useDispatch, useSelector } from 'react-redux';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
@@ -10,13 +11,16 @@ import ResponseHeaders from './ResponseHeaders';
import StatusCode from './StatusCode';
import ResponseTime from './ResponseTime';
import ResponseSize from './ResponseSize';
import Timeline from './Timeline';
import TestResults from './TestResults';
import TestResultsLabel from './TestResultsLabel';
import StyledWrapper from './StyledWrapper';
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isLoading = item.response && item.response.state === 'sending';
const isLoading = ['queued', 'sending'].includes(item.requestState);
const selectTab = (tab) => {
dispatch(
@@ -32,11 +36,27 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const getTabPanel = (tab) => {
switch (tab) {
case 'response': {
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
return (
<QueryResult
item={item}
collection={collection}
width={rightPaneWidth}
value={
response.data ? (isJson(response.headers) ? safeStringifyJSON(response.data, true) : response.data) : ''
}
mode={getContentType(response.headers)}
/>
);
}
case 'headers': {
return <ResponseHeaders headers={response.headers} />;
}
case 'timeline': {
return <Timeline request={item.requestSent} response={item.response} />;
}
case 'tests': {
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
}
default: {
return <div>404 | Not found</div>;
@@ -66,7 +86,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
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) => {
@@ -75,6 +95,28 @@ 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">
@@ -84,6 +126,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
Timeline
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
<TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
<StatusCode status={response.status} />

View File

@@ -0,0 +1,38 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
.some-tests-failed {
color: ${(props) => props.theme.colors.text.danger} !important;
}
.all-tests-passed {
color: ${(props) => props.theme.colors.text.green} !important;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import get from 'lodash/get';
import classnames from 'classnames';
import { safeStringifyJSON } from 'utils/common';
import QueryResult from 'components/ResponsePane/QueryResult';
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
import StatusCode from 'components/ResponsePane/StatusCode';
import ResponseTime from 'components/ResponsePane/ResponseTime';
import ResponseSize from 'components/ResponsePane/ResponseSize';
import Timeline from 'components/ResponsePane/Timeline';
import TestResults from 'components/ResponsePane/TestResults';
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
import StyledWrapper from './StyledWrapper';
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const [selectedTab, setSelectedTab] = useState('response');
const { requestSent, responseReceived, testResults } = item;
const headers = get(item, 'responseReceived.headers', {});
const status = get(item, 'responseReceived.status', 0);
const size = get(item, 'responseReceived.size', 0);
const duration = get(item, 'responseReceived.duration', 0);
const selectTab = (tab) => setSelectedTab(tab);
const getTabPanel = (tab) => {
switch (tab) {
case 'response': {
return (
<QueryResult
item={item}
collection={collection}
width={rightPaneWidth}
disableRunEventListener={true}
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
/>
);
}
case 'headers': {
return <ResponseHeaders headers={headers} />;
}
case 'timeline': {
return <Timeline request={requestSent} response={responseReceived} />;
}
case 'tests': {
return <TestResults results={testResults} />;
}
default: {
return <div>404 | Not found</div>;
}
}
};
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === selectedTab
});
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center px-3 tabs" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
Response
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
Timeline
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
<TestResultsLabel results={testResults} />
</div>
<div className="flex flex-grow justify-end items-center">
<StatusCode status={status} />
<ResponseTime duration={duration} />
<ResponseSize size={size} />
</div>
</div>
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
</StyledWrapper>
);
};
export default ResponsePane;

View File

@@ -0,0 +1,31 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.item-path {
.link {
color: ${(props) => props.theme.textLink};
}
}
.danger {
color: ${(props) => props.theme.colors.text.danger};
}
.test-summary {
color: ${(props) => props.theme.tabs.active.border};
}
/* test results */
.test-success {
color: ${(props) => props.theme.colors.text.green};
}
.test-failure {
color: ${(props) => props.theme.colors.text.danger};
.error-message {
color: ${(props) => props.theme.colors.text.muted};
}
}
`;
export default Wrapper;

View File

@@ -0,0 +1,232 @@
import React, { useState, useEffect } from 'react';
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 { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
import slash from 'utils/common/slash';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
const getRelativePath = (fullPath, pathname) => {
// convert to unix style path
fullPath = slash(fullPath);
pathname = slash(pathname);
let relativePath = path.relative(fullPath, pathname);
const { dir, name } = path.parse(relativePath);
return path.join(dir, name);
};
export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if (!collection.runnerResult) {
setSelectedItem(null);
}
}, [collection, setSelectedItem]);
const collectionCopy = cloneDeep(collection);
const items = cloneDeep(get(collection, 'runnerResult.items', []));
const runnerInfo = get(collection, 'runnerResult.info', {});
each(items, (item) => {
const info = findItemInCollection(collectionCopy, item.uid);
item.name = info.name;
item.type = info.type;
item.filename = info.filename;
item.pathname = info.pathname;
item.relativePath = getRelativePath(collection.pathname, info.pathname);
if (item.status !== 'error') {
if (item.testResults) {
const failed = item.testResults.filter((result) => result.status === 'fail');
item.testStatus = failed.length ? 'fail' : 'pass';
} else {
item.testStatus = 'pass';
}
if (item.assertionResults) {
const failed = item.assertionResults.filter((result) => result.status === 'fail');
item.assertionStatus = failed.length ? 'fail' : 'pass';
} else {
item.assertionStatus = 'pass';
}
}
});
const runCollection = () => {
dispatch(runCollectionFolder(collection.uid, null, true));
};
const runAgain = () => {
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
};
const closeRunner = () => {
dispatch(
closeCollectionRunner({
collectionUid: collection.uid
})
);
};
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
const passedRequests = items.filter((item) => {
return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
});
const failedRequests = items.filter((item) => {
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
});
if (!items || !items.length) {
return (
<StyledWrapper className="px-4">
<div className="font-medium mt-6 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
<div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div>
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
Run Collection
</button>
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
Close
</button>
</StyledWrapper>
);
}
return (
<StyledWrapper className="px-4">
<div className="font-medium mt-6 mb-4 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
<div className="flex">
<div className="flex flex-col flex-1">
<div className="py-2 font-medium test-summary">
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
</div>
{items.map((item) => {
return (
<div key={item.uid}>
<div className="item-path mt-2">
<div className="flex items-center">
<span>
{item.status !== 'error' && item.testStatus === 'pass' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
) : (
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
)}
</span>
<span
className={`mr-1 ml-2 ${item.status == 'error' || item.testStatus == 'fail' ? 'danger' : ''}`}
>
{item.relativePath}
</span>
{item.status !== 'error' && item.status !== 'completed' ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
) : (
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
(<span className="mr-1">{get(item.responseReceived, 'status')}</span>
<span>{get(item.responseReceived, 'statusText')}</span>)
</span>
)}
</div>
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
<ul className="pl-8">
{item.testResults
? item.testResults.map((result) => (
<li key={result.uid}>
{result.status === 'pass' ? (
<span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2" />
{result.description}
</span>
) : (
<>
<span className="test-failure flex items-center">
<IconX size={18} strokeWidth={2} className="mr-2" />
{result.description}
</span>
<span className="error-message pl-8 text-xs">{result.error}</span>
</>
)}
</li>
))
: null}
{item.assertionResults
? item.assertionResults.map((result) => (
<li key={result.uid}>
{result.status === 'pass' ? (
<span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2" />
{result.lhsExpr}: {result.rhsExpr}
</span>
) : (
<>
<span className="test-failure flex items-center">
<IconX size={18} strokeWidth={2} className="mr-2" />
{result.lhsExpr}: {result.rhsExpr}
</span>
<span className="error-message pl-8 text-xs">{result.error}</span>
</>
)}
</li>
))
: null}
</ul>
</div>
</div>
);
})}
{runnerInfo.status === 'ended' ? (
<div className="mt-2 mb-4">
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
Run Again
</button>
<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>
</div>
) : null}
</div>
<div className="flex flex-1" style={{ width: '50%' }}>
{selectedItem ? (
<div className="flex flex-col w-full overflow-auto">
<div className="flex items-center px-3 mb-4 font-medium">
<span className="mr-2">{selectedItem.relativePath}</span>
<span>
{selectedItem.testStatus === 'pass' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
) : (
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
)}
</span>
</div>
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
<ResponsePane item={selectedItem} collection={collection} />
</div>
) : null}
</div>
</div>
</StyledWrapper>
);
}

View File

@@ -1,4 +1,5 @@
import React, { useRef, useEffect } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
@@ -16,11 +17,19 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
name: item.name
},
validationSchema: Yup.object({
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(cloneItem(values.name, item.uid, collection.uid));
onClose();
dispatch(cloneItem(values.name, item.uid, collection.uid))
.then(() => {
onClose();
})
.catch((err) => {
toast.error(err ? err.message : 'An error occured while cloning the request');
});
}
});
@@ -33,7 +42,13 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
const onSubmit = () => formik.handleSubmit();
return (
<Modal size="sm" title={`Clone ${isFolder ? 'Folder' : 'Request'}`} confirmText="Clone" handleConfirm={onSubmit} handleCancel={onClose}>
<Modal
size="sm"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
confirmText="Clone"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">

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