Compare commits

...

413 Commits

Author SHA1 Message Date
Anoop M D
5fc32d035f wip: working on async scripting support 2023-03-28 14:49:57 +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
Vinod Godti
cffef31f97 RequestPane body form input text color visibility fix in dark mode (#70) 2022-11-10 18:29:17 +05:30
Nash
b93be5a846 Set default theme to the user's browser theme (#69) 2022-11-09 22:55:30 +05:30
Anoop M D
544765af3e fix: fixed graphql docs height overflow (#65) 2022-11-08 03:45:30 +05:30
Anoop M D
2393092248 feat: graphql docs explorer (#65) 2022-11-08 03:35:58 +05:30
Anoop M D
dcdeb78995 release: bruno-graphql-docs@0.1.0 2022-11-08 03:34:25 +05:30
Ankur Singh Chauhan
e16650d4a7 Bugfix/split line (#66)
* Issue:62,fix Split line should have max squeeze limit
* added limit to right pane also
2022-11-07 18:22:09 +05:30
Anoop M D
62ed489847 chore: bumped bruno-schema in bruno-app 2022-11-07 03:14:48 +05:30
Anoop M D
2930eb29ec chore: release bruno-schema 0.2.0 2022-11-07 03:13:22 +05:30
Anoop M D
eecb60f5cf Merge branch 'main' of github.com:usebruno/bruno into main 2022-11-07 03:06:18 +05:30
Ankur Singh Chauhan
82fb2819c2 Issue:62,fix Split line should have max squeeze limit (#64)
Co-authored-by: Ankur Singh Chauhan <anx450z@gmail.com>
2022-11-07 03:03:35 +05:30
Anoop M D
2aef7c61a4 feat: graphql support (#65) 2022-11-07 02:56:58 +05:30
Anoop M D
530af1f929 chore: fix peer deps issue for rollup 2022-11-06 14:41:10 +05:30
Anoop M D
3753fd1e20 feat: standalone graphiql docs explorer 2022-11-06 01:04:30 +05:30
Anoop M D
a59ae75809 fix: fixed postman import url issue 2022-11-05 01:00:21 +05:30
Anoop M D
4c18c27406 chore: allow legacy peer deps while while running playwright tests 2022-11-05 00:21:15 +05:30
Anoop M D
5d25fdcf7a chore: bumped rollup version 2022-11-05 00:16:54 +05:30
Anoop M D
8cfdb3ebcb Merge branch 'main' of github.com:usebruno/bruno 2022-11-04 23:42:43 +05:30
Anoop M D
9e64ea5439 feat: package init for graphql-docs 2022-11-04 23:42:34 +05:30
shash68i
23c8044973 chore: prettier script added in root bruno/package.json (#63) 2022-11-02 15:22:48 +05:30
shash68i
46ac15dd81 Collection items whole div made as action button and cursor pointer added to the items (#61) 2022-11-01 00:50:51 +05:30
depa panjie purnama
5ad9be4f6b feat: Create New Request e2e test (#52)
* add selector ID
* add createNewRequest flow
* selector update
2022-10-31 16:50:57 +05:30
shash68i
f46625c689 Added missing <br /> between Website and Discord link in readme.md (#60) 2022-10-31 16:49:27 +05:30
Anoop M D
c0e1bf6bc2 chore: added discord invite link in readme 2022-10-31 03:14:29 +05:30
Anoop M D
874ca07f39 chore: added discord link 2022-10-31 00:58:36 +05:30
Anoop M D
a291e7f345 fix: fixed mac electron build issues 2022-10-31 00:58:20 +05:30
Anoop M D
c9cabfde35 release: bumped version 2022-10-30 20:07:11 +05:30
Anoop M D
fde15d7c31 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-30 19:58:06 +05:30
Anoop M D
11defe18ca chore: disable telemetry during playwright test execution 2022-10-30 19:56:54 +05:30
anusreesubash
54b14a005d fix(#53): fix for response editor search issue (#55) 2022-10-30 17:50:38 +05:30
Anoop M D
f283df2a1b feat: posthog telemetry 2022-10-30 17:48:36 +05:30
Anoop M D
820c99711b fix: fixed missing sidebar bg color 2022-10-30 04:05:52 +05:30
Anoop M D
df1cd4aff9 Merge branch 'feature/import-postman-collection' 2022-10-30 02:01:22 +05:30
Anoop M D
481486cd1c feat: import postman collection (#45) 2022-10-30 02:00:54 +05:30
Anoop M D
bf4c26de33 feat: refactored import collection 2022-10-30 00:09:24 +05:30
depa panjie purnama
c3fa473dae use fakerjs as random test data (#51) 2022-10-27 23:23:04 +05:30
depa panjie purnama
90a29918d0 feat: Create Collection e2e test (#50)
* add selector IDs
* add Create Collection e2e test
2022-10-26 21:59:37 +05:30
Anoop M D
c0698adcb3 chore: cleanup 2022-10-25 14:58:57 +05:30
Anoop M D
0d0f99e810 chore: cleanup 2022-10-25 14:57:53 +05:30
Anoop M D
7f5a6d5566 chore: using npm i instead of ci in playwright github actions 2022-10-25 00:52:42 +05:30
depa panjie purnama
dc68d511bd add e2e test using playwright (#44) 2022-10-25 00:42:53 +05:30
Anoop M D
0fceaf6918 chore: updated readme 2022-10-23 22:40:26 +05:30
Anoop M D
831223711a chore: updated app description 2022-10-23 20:57:35 +05:30
Anoop M D
e4cf3750bd chore: fixed missing screenshote in readme 2022-10-23 20:51:12 +05:30
Anoop M D
01e15b7fc1 Merge branch 'main' of github.com:usebruno/bruno 2022-10-23 19:48:33 +05:30
Anoop M D
3bf3d30ce8 fix: environment var interpolation issues 2022-10-23 19:48:02 +05:30
Vijay Hudge
bf6e6b29f5 Updated Readme with all contributors shown as image (#43) 2022-10-23 17:17:45 +05:30
Sean
075aaaebec Fixed spelling error in contributing.md and updated pull request section (#41) 2022-10-23 13:50:42 +05:30
Anoop M D
1136f1b105 release: v0.2.0 2022-10-23 13:43:12 +05:30
Anoop M D
5c8e66b684 feat: using a lighter shade of blue for dark theme 2022-10-23 12:53:24 +05:30
Anoop M D
09faf46635 chore: updated bruno tagline 2022-10-23 12:39:19 +05:30
Anoop M D
ef28637d0c Merge branch 'feature/dark-mode' 2022-10-23 12:37:20 +05:30
Anoop M D
51784d08cd fix: fixed bugs 2022-10-23 11:57:35 +05:30
Anoop M D
96f50b0c6d feat: dark-mode completed :) 2022-10-23 11:26:16 +05:30
Anoop M D
2ba6e4823d feat: dark-mode (response status and support modal) 2022-10-23 05:32:47 +05:30
Anoop M D
04a0a37ca4 feat: dark-mode (code editor) 2022-10-23 04:48:43 +05:30
Anoop M D
23400a77f8 feat: dark mode (tabs and request tabs) 2022-10-23 03:21:23 +05:30
Anoop M D
4718c77e3d feat: dark model (modals) 2022-10-23 02:14:59 +05:30
anusreesubash
0cde789697 fix: fixed issue renaming workspaces and creating collections (#40) 2022-10-22 16:32:42 +05:30
Anoop M D
6be2818bfb feat: dark mode (environment selector, query url) 2022-10-22 16:00:04 +05:30
Anoop M D
ac4e3a9f3d chore: rollback temporary fix to make the vercel build pass 2022-10-22 00:47:24 +05:30
Anoop M D
0ecaba27a6 fix: temporary fix for failing vercel build since its not finding nextjs in root 2022-10-22 00:37:55 +05:30
Anoop M D
2c8ef7b626 chore: bumped nextjs to 12.3.1 2022-10-22 00:34:01 +05:30
Bram Hoven
ea3a9394c9 Improve error for workspace deletion (#39)
* Move dmg-license to optionalDependencies so it can be installed on windows
* Extend toastError to accept a default error message
* Throw BrunoError when in deleteWorkspace
* Handle errors with the toastError
* Use existing parseError for getting errorMsg in toastError
2022-10-21 23:57:42 +05:30
Anoop M D
995c6b3fd0 chore: updated landing image 2022-10-21 18:46:23 +05:30
Anoop M D
bd6ce6a67b feat: added contributing guide 2022-10-21 18:41:28 +05:30
Anoop M D
8e70e191e1 feat: darkmode (sidebar, menubar and welcome page #23) 2022-10-21 04:20:23 +05:30
Sean
cbdfabb4db Features/dark mode (#37)
* Added use local storage hook
* Added theme
* Added theme support
* Added theme provider
* Added dark theme for sidebar
* Added dark theme for main content area
* Added theme
* Added theme support
* Added theme provider
* Added dark theme for sidebar
* Added dark theme for main content area
2022-10-21 00:44:09 +05:30
Sean
3c3c9a6026 Added use local storage hook (#36) 2022-10-20 16:30:35 +05:30
Anoop M D
6b68857b81 chore: added package-lock.json to tauri and testbench packages 2022-10-20 15:18:09 +05:30
Anoop M D
405f253eef chore: deleted package-lock.json in tauri and testbench packages 2022-10-20 15:17:16 +05:30
Anoop M D
4b6439785f chore: added package-lock.json to gitignore 2022-10-20 15:16:33 +05:30
Anoop M D
f9806e69a5 Merge branch 'main' of github.com:usebruno/bruno 2022-10-20 15:11:15 +05:30
Anoop M D
ba219d66db feat: prettier config 2022-10-20 15:09:30 +05:30
Sean
a41f4fe024 fix: dependency error when contributing (#34) 2022-10-19 19:34:42 +05:30
Anoop M D
93544f8ae6 chore: fix typo 2022-10-18 05:33:57 +05:30
Anoop M D
d6e4d07e2c chore: collections page disabled until fix is available in electron 2022-10-18 05:12:56 +05:30
Anoop M D
503f0b8a17 feat: electron windows build 2022-10-18 05:05:49 +05:30
Anoop M D
5fc9bbd729 fix: fixed issue while saving json in local collections 2022-10-18 04:26:28 +05:30
Anoop M D
eefef27dec feat: error messaging when attempting to create duplicate files or folders in local collections 2022-10-18 03:44:21 +05:30
Anoop M D
e98f219448 feat: local collections environment sync 2022-10-18 03:39:36 +05:30
Anoop M D
ff87586a1d feat: bruno.json validation for local collections 2022-10-18 02:34:22 +05:30
Anoop M D
8a96a0ce71 feat: wireup local collections open and create buttons 2022-10-18 01:27:53 +05:30
Anoop M D
9fae7f72d4 feat: electron build for linux 2022-10-17 22:17:03 +05:30
Anoop M D
ad1824e473 fix: star button no loading in electron 2022-10-17 22:02:55 +05:30
Anoop M D
8045751671 chore: svgs are moved inside src folder 2022-10-17 21:40:57 +05:30
Anoop M D
c258bc1590 feat: electron build for mac 2022-10-17 21:04:05 +05:30
Anoop M D
075e9162c2 feat: chrome extension migrate to manifest v3 2022-10-17 18:18:27 +05:30
Anoop M D
d91ee36192 feat: chrome extension 2022-10-17 03:57:25 +05:30
Anoop M D
579bd424fc chore: added bruno-cli package 2022-10-17 03:07:15 +05:30
Anoop M D
78645ad52f feat: published @usebruno/schema 2022-10-17 03:03:32 +05:30
Anoop M D
241ee5e788 feat: sample collection + bug fixes 2022-10-17 00:39:58 +05:30
Anoop M D
6573df41b0 feat: hotkeys (ctrl.cmd E, B, H) 2022-10-16 23:58:04 +05:30
Anoop M D
fe900b90c9 feat: version number in collection schema 2022-10-16 23:36:10 +05:30
Anoop M D
7078d5cec2 feat: beteer cors error messaging 2022-10-16 22:42:29 +05:30
Anoop M D
46949e48ba feat: wip packaging chrome extension 2022-10-16 20:01:23 +05:30
Anoop M D
abc00b810f feat: interpolate environment vats while sending request 2022-10-16 19:25:47 +05:30
Anoop M D
510e549d34 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-16 18:51:07 +05:30
Anoop M D
42a60a3372 feat: persist selected environment inside collection 2022-10-16 18:51:02 +05:30
anusreesubash
f1aaf862ae Feature/support links (#30)
* feat: added support links
* chore: fixed lint issues
2022-10-16 18:29:00 +05:30
Anoop M D
ecc2252e84 feat: persist active workspace in local storage 2022-10-16 17:24:30 +05:30
Anoop M D
2efc11ff6b feat: environment variables grid 2022-10-16 16:40:54 +05:30
Anoop M D
6a36313e0e fix: fixed error during item rename 2022-10-16 14:51:47 +05:30
Anoop M D
6380797f92 fix: fixed bug where collection was not getting created 2022-10-16 14:40:43 +05:30
Anoop M D
d8f58aeb0d chore: local collections unavailable msg on web 2022-10-16 14:34:09 +05:30
Anoop M D
0f5b75ddbf chore: updated readme 2022-10-16 13:51:10 +05:30
Anoop M D
7ca6270f2b feat: connect environments to redux store 2022-10-16 05:46:49 +05:30
Anoop M D
c6ac90a9f8 Merge branch 'feature/environment-configuration' into main 2022-10-16 04:05:59 +05:30
Anoop M D
d640dafb06 chore: collection uid check before pushing collections into state 2022-10-16 04:03:00 +05:30
Anoop M D
d8cdd2ad8b feat: request cancel implementation in electron 2022-10-16 03:06:46 +05:30
Anoop M D
118658822d feat: strike of collection from electron-store upon removal 2022-10-16 02:21:48 +05:30
Anoop M D
0709666b03 feat: woekspace switch modal 2022-10-16 02:18:56 +05:30
Anoop M D
fad953a983 feat: auto hydrate last opened collections 2022-10-16 02:06:58 +05:30
Anusree Subash
4a5378a2e1 feat: environment configuration (resolves #8) 2022-10-16 01:15:36 +05:30
Anoop M D
75f6daec06 chore: cleanup 2022-10-16 01:07:45 +05:30
Anoop M D
f2ffca35da feat: local collections displayed separately (resolves #22) 2022-10-16 01:05:52 +05:30
Anoop M D
c95bc8fdf9 feat: remove local collection from workspace (resolves #22) 2022-10-15 21:22:25 +05:30
Anoop M D
44aa019754 feat: local filesystem collections (resolves #22) 2022-10-15 20:14:43 +05:30
Anoop M D
91981a48e4 feat: collections are stored as objects in workspaces 2022-10-15 13:50:58 +05:30
Anoop M D
d546709b26 feat: import collections 2022-10-15 04:04:45 +05:30
Anoop M D
6feca9937e feat: export collections 2022-10-15 03:07:50 +05:30
Anoop M D
4ff268712f feat: added schema validation before saving collections to idb 2022-10-15 02:48:06 +05:30
Anoop M D
a78bdf87fe redactor: moved all reducer collection actions needing idb access to actions file 2022-10-15 02:08:35 +05:30
Anoop M D
a84080b482 fix: activeTabUid was not being reset to null 2022-10-15 01:45:15 +05:30
Anoop M D
b3bf29d6b2 feat: collection schema definition 2022-10-15 01:15:56 +05:30
Anoop M D
013f9f9e3d feat: url bar now occupies full width of the panel 2022-10-15 00:35:00 +05:30
Anoop M D
e46e3d5b22 feat: yup schema should not allow unknown keys 2022-10-14 22:42:46 +05:30
Anoop M D
8763ff2ad1 feat: glue workspace schema validations 2022-10-14 22:35:02 +05:30
Anoop M D
7ddfac1ece feat: bruno schema definition for workspace 2022-10-14 21:51:59 +05:30
Anoop M D
6bb3967379 feat: improved tab behaviour while closing a tab 2022-10-14 02:12:59 +05:30
Anoop M D
d49eb4df33 chore: cleanup 2022-10-14 01:59:24 +05:30
Anoop M D
410bc70318 feat: cancel running request (resolves #26) 2022-10-14 01:34:15 +05:30
Anoop M D
097a6240ad chore: refactor request type names 2022-10-14 00:43:03 +05:30
Anoop M D
6b0ccac1bf feat: support adding and removing collections from workspaces 2022-10-14 00:20:02 +05:30
Anoop M D
f8fbc88239 chore: moved next deps to bruno-app package 2022-10-13 22:08:36 +05:30
Anoop M D
819e8c2ccd feat: sync workspaces with idb 2022-10-13 05:23:54 +05:30
Anoop M D
008704c4e1 feat: load workspaces from idb 2022-10-12 04:41:26 +05:30
Anoop M D
1b2097250e chrore: added icon for reporting issues 2022-10-11 03:56:27 +05:30
Anoop M D
42984ce931 chore: updated bruno-schema readme 2022-10-11 03:50:48 +05:30
Anoop M D
aed737ed33 chore: cleanup 2022-10-11 03:42:48 +05:30
Anoop M D
0bd51b8a01 feat: bruno schema init 2022-10-11 03:39:26 +05:30
Anoop M D
02ff85cc57 feat: delete collections 2022-10-11 03:20:50 +05:30
Anoop M D
adc6be031d feat: toast integration 2022-10-11 02:29:19 +05:30
Anoop M D
6476b47d53 feat: rename collection 2022-10-11 02:11:52 +05:30
Anoop M D
62e9f4d5f0 chore: added null safety while searching 2022-10-11 01:35:46 +05:30
Anoop M D
77568da03c feat: show home page upon clicking title bar 2022-10-11 01:28:31 +05:30
Anoop M D
be72fbfe6f chore: cleanup 2022-10-10 23:27:27 +05:30
Anoop M D
b9ab5e572d chore: updated request tab method name font size 2022-10-10 05:34:04 +05:30
Anoop M D
09c6feed98 chore: updated readme 2022-10-10 04:26:47 +05:30
Anoop M D
a54d6fe6d7 feat: layout redesign 2022-10-10 04:10:45 +05:30
Anoop M D
9c5d66e7db chore: added comparision table in readme 2022-10-10 01:37:39 +05:30
Anoop M D
bacc8a1084 chore: added links in home page 2022-10-10 00:01:04 +05:30
Anoop M D
50ae592e1e feat: collections page (resolves #24) 2022-10-09 23:15:46 +05:30
Anoop M D
c8957f5555 chore: placeholder for local collections in welcome page (#22) 2022-10-09 22:20:52 +05:30
Anoop M D
fba3f24568 chore: updated icon used for collection 2022-10-09 16:32:44 +05:30
Anoop M D
539cdef9ca feat: moved next app to its own package (resolves #21) 2022-10-09 16:13:11 +05:30
anusreesubash
b3a317dc4d feat: workspaces crud (resolves #15) (#19)
feat: workspaces crud (resolves #15)
2022-10-09 12:45:48 +05:30
Anoop M D
f634839adb feat: tauri builder package (resolves #18) 2022-10-09 12:34:09 +05:30
Anoop M D
2ac1b3639d feat: moved bruno testbench into packages/ (feat: resolves #17) 2022-10-08 20:00:36 +05:30
Anoop M D
64bffc8216 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-08 19:53:49 +05:30
Anoop M D
1dd808ed20 feat: package for electron-builder (resolves #16) 2022-10-08 19:53:30 +05:30
482 changed files with 27026 additions and 24470 deletions

27
.github/workflows/playwright.yml vendored Normal file
View File

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

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

12
.gitignore vendored
View File

@@ -2,6 +2,9 @@
# dependencies
node_modules
yarn.lock
pnpm-lock.yaml
package-lock.json
.pnp
.pnp.js
@@ -10,6 +13,11 @@ coverage
# production
build
chrome-extension
chrome-extension.pem
chrome-extension.crx
bruno.zip
*.zip
# misc
.DS_Store
@@ -27,5 +35,9 @@ yarn-error.log*
.env.production.local
# next.js
/renderer
/renderer/.next/
/renderer/out/
/test-results/
/playwright-report/
/playwright/.cache/

2
.nvmrc
View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

35
contributing.md Normal file
View File

@@ -0,0 +1,35 @@
## Lets make bruno better, together !!
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
### Technology Stack
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### Dependencies
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
### Lets start coding
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
### Raising Pull Request
- Please keep the PR's small and focused on one thing
- Please follow the format of creating branches
- feature/[feature name]: This branch should contain changes for a specific feature
- Example: feature/dark-mode
- bugfix/[bug name]: This branch should container only bug fixes for a specific bug
- Example bugfix/bug-1

View File

@@ -1 +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 --legacy-peer-deps
# build graphql docs
# note: you can for now ignore the error thrown while building the graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### 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.
```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

@@ -1,47 +0,0 @@
const axios = require('axios');
const FormData = require('form-data');
const { ipcMain } = require('electron');
const { forOwn, extend } = require('lodash');
const registerIpc = () => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, request) => {
try {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
});
extend(request.headers, form.getHeaders());
request.data = form;
}
const result = await axios(request);
return {
status: result.status,
headers: result.headers,
data: result.data
};
} catch (error) {
if(error.response) {
return {
status: error.response.status,
headers: error.response.headers,
data: error.response.data
};
}
return {
status: -1,
headers: [],
data: null
};
}
});
};
module.exports = registerIpc;

18494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,38 @@
{
"name": "grafnode",
"name": "usebruno",
"private": true,
"main": "main/index.js",
"scripts": {
"clean": "rimraf dist renderer/.next renderer/out",
"start": "electron .",
"build": "next build renderer && next export renderer",
"pack-app": "npm run build && electron-builder --dir",
"dist": "npm run build && electron-builder"
},
"build": {
"asar": true,
"files": [
"main",
"renderer/out"
]
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"codemirror-graphql": "^1.2.5",
"electron-is-dev": "^2.0.0",
"electron-next": "^3.1.5",
"electron-store": "^8.0.1",
"electron-util": "^0.17.2",
"escape-html": "^1.0.3",
"form-data": "^4.0.0",
"formik": "^2.2.9",
"fs-extra": "^10.0.1",
"graphiql": "^1.5.9",
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.12",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"markdown-it": "^12.2.0",
"mousetrap": "^1.6.5",
"nanoid": "^3.1.30",
"next": "12.0.4",
"qs": "^6.11.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"
},
"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"
],
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-spread": "^7.16.7",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"babel-loader": "^8.2.3",
"css-loader": "^6.5.1",
"electron": "^17.1.0",
"electron-builder": "^22.14.13",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"next": "^12.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"style-loader": "^3.3.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"jest": "^29.2.0",
"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:electron": "./scripts/build-electron.sh",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report"
},
"overrides": {
"rollup": "3.2.5"
}
}
}

View File

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

34
packages/bruno-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
pnpm-lock.yaml
package-lock.json
yarn.lock
# testing
coverage
# production
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# next.js
.next/
out/

View File

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

View File

@@ -4,7 +4,10 @@
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"assets/*": ["src/assets/*"],
"components/*": ["src/components/*"],
"hooks/*": ["src/hooks/*"],
"themes/*": ["src/themes/*"],
"api/*": ["src/api/*"],
"pageComponents/*": ["src/pageComponents/*"],
"providers/*": ["src/providers/*"],

View File

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

View File

@@ -0,0 +1,75 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"private": true,
"scripts": {
"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": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.3.1",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"codemirror-graphql": "^1.2.5",
"escape-html": "^1.0.3",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"graphiql": "^1.5.9",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"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-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-spread": "^7.16.7",
"@babel/preset-env": "^7.16.4",
"@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",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.7.1",
"style-loader": "^3.3.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,30 @@
const darkTheme = {
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': '#ffffff',
'primary-theme': '#1e1e1e',
'secondary-text': '#929292',
'sidebar-collection-item-active-indent-border': '#d0d0d0',
'sidebar-collection-item-active-background': '#e1e1e1',
'sidebar-background': '#252526',
'sidebar-bottom-bg': '#68217a',
'request-dragbar-background': '#efefef',
'request-dragbar-background-active': 'rgb(200, 200, 200)',
'tab-inactive': 'rgb(155 155 155)',
'tab-active-border': '#546de5',
'layout-border': '#dedede',
'codemirror-border': '#efefef',
'codemirror-background': 'rgb(243, 243, 243)',
'text-link': '#1663bb',
'text-danger': 'rgb(185, 28, 28)',
'background-danger': '#dc3545',
'method-get': 'rgb(5, 150, 105)',
'method-post': '#8e44ad',
'method-delete': 'rgb(185, 28, 28)',
'method-patch': 'rgb(52 52 52)',
'method-options': 'rgb(52 52 52)',
'method-head': 'rgb(52 52 52)',
'table-stripe': '#f3f3f3'
};
export default darkTheme;

View File

@@ -0,0 +1,7 @@
import darkTheme from './dark';
import lightTheme from './light';
export default {
Light: lightTheme,
Dark: darkTheme
};

View File

@@ -0,0 +1,30 @@
const lightTheme = {
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': 'rgb(52 52 52)',
'primary-theme': '#ffffff',
'secondary-text': '#929292',
'sidebar-collection-item-active-indent-border': '#d0d0d0',
'sidebar-collection-item-active-background': '#e1e1e1',
'sidebar-background': '#f3f3f3',
'sidebar-bottom-bg': '#f3f3f3',
'request-dragbar-background': '#efefef',
'request-dragbar-background-active': 'rgb(200, 200, 200)',
'tab-inactive': 'rgb(155 155 155)',
'tab-active-border': '#546de5',
'layout-border': '#dedede',
'codemirror-border': '#efefef',
'codemirror-background': 'rgb(243, 243, 243)',
'text-link': '#1663bb',
'text-danger': 'rgb(185, 28, 28)',
'background-danger': '#dc3545',
'method-get': 'rgb(5, 150, 105)',
'method-post': '#8e44ad',
'method-delete': 'rgb(185, 28, 28)',
'method-patch': 'rgb(52 52 52)',
'method-options': 'rgb(52 52 52)',
'method-head': 'rgb(52 52 52)',
'table-stripe': '#f3f3f3'
};
export default lightTheme;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path fill="#515151" d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z"/>
<path d="M0 0h48v48h-48z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,94 @@
import React from 'react';
const Bruno = ({ width }) => {
return (
<svg id="emoji" width={width} viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path
fill="#F4AA41"
stroke="none"
d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"
/>
<polygon
fill="#EA5A47"
stroke="none"
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855" />
</g>
<g id="hair" />
<g id="skin" />
<g id="skin-shadow" />
<g id="line">
<path
fill="#000000"
stroke="none"
d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"
/>
<path
fill="#000000"
stroke="none"
d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632" />
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" />
</g>
</svg>
);
};
export default Bruno;

View File

@@ -1,11 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.create-request {
color: #737373;
font-size: 0.75rem;
}
color: ${(props) => props.theme.text};
.collection-options {
svg {
position: relative;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import Modal from 'components/Modal/index';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const BrunoSupport = ({ onClose }) => {
return (
<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} />
<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>
</Modal>
</StyledWrapper>
);
};
export default BrunoSupport;

View File

@@ -0,0 +1,45 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
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
.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-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
color: #b5cea8 !important;
}
.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() {
@@ -37,36 +41,41 @@ export default class QueryEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
readOnly: this.props.readOnly ? 'nocursor' : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly,
scrollbarStyle: "overlay",
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Tab': function(cm){
cm.replaceSelection(" " , "end");
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
}
},
}
}));
if (editor) {
editor.on('change', this._onEdit);
this.addOverlay();
}
}
@@ -82,14 +91,20 @@ export default class QueryEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
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) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
this.ignoreChangeEvent = false;
}
@@ -106,13 +121,22 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full"
aria-label="Code Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>
);
}
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

@@ -0,0 +1,53 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.dropdown-toggle {
&:hover {
color: black;
}
}
.tippy-box {
min-width: 135px;
font-size: 0.8125rem;
color: ${(props) => props.theme.dropdown.color};
background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.dropdown.shadow};
border-radius: 3px;
.tippy-content {
padding-left: 0;
padding-right: 0;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
.label-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
background-color: ${(props) => props.theme.dropdown.labelBg};
}
.dropdown-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
cursor: pointer;
.icon {
color: ${(props) => props.theme.dropdown.iconColor};
}
&:hover {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
}
}
}
}
`;
export default Wrapper;

View File

@@ -0,0 +1,15 @@
import React from 'react';
import Tippy from '@tippyjs/react';
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">
{icon}
</Tippy>
</StyledWrapper>
);
};
export default Dropdown;

View File

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

View File

@@ -0,0 +1,82 @@
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, IconDatabaseOff } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const EnvironmentSelector = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const { environments, activeEnvironmentUid } = collection;
const activeEnvironment = activeEnvironmentUid ? find(environments, (e) => e.uid === activeEnvironmentUid) : null;
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="current-enviroment flex items-center justify-center pl-3 pr-2 py-1 select-none">
{activeEnvironment ? activeEnvironment.name : 'No Environment'}
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);
});
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const onSelect = (environment) => {
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
} else {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occured while selecting the environment'));
};
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer environment-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
{environments && environments.length
? environments.map((e) => (
<div
className="dropdown-item"
key={e.uid}
onClick={() => {
onSelect(e);
dropdownTippyRef.current.hide();
}}
>
<IconDatabase size={18} strokeWidth={1.5} /> <span className="ml-2">{e.name}</span>
</div>
))
: null}
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onSelect(null);
}}
>
<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>Configure</span>
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)} />}
</StyledWrapper>
);
};
export default EnvironmentSelector;

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
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')
}),
onSubmit: (values) => {
dispatch(addEnvironment(values.name, collection.uid))
.then(() => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occured while created the environment'));
}
});
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
};
return (
<Portal>
<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">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
</Portal>
);
};
export default CreateEnvironment;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import Portal from 'components/Portal/index';
import toast from 'react-hot-toast';
import Modal from 'components/Modal/index';
import { deleteEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const DeleteEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
const onConfirm = () => {
dispatch(deleteEnvironment(environment.uid, collection.uid))
.then(() => {
toast.success('Environment deleted successfully');
onClose();
})
.catch(() => toast.error('An error occured while deleting the environment'));
};
return (
<Portal>
<StyledWrapper>
<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>
</Portal>
);
};
export default DeleteEnvironment;

View File

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};;
font-size: 0.8125rem;
user-select: none;
}
@@ -22,23 +23,21 @@ const Wrapper = styled.div`
.btn-add-param {
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;
}
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: transparent;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,129 @@
import React, { useReducer } from 'react';
import toast from 'react-hot-toast';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import reducer from './reducer';
import StyledWrapper from './StyledWrapper';
const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch();
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
const { variables, hasChanges } = state;
const saveChanges = () => {
dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid))
.then(() => {
toast.success('Changes saved successfully');
reducerDispatch({
type: 'CHANGES_SAVED'
});
})
.catch(() => toast.error('An error occured while saving the changes'));
};
const addVariable = () => {
reducerDispatch({
type: 'ADD_VAR'
});
};
const handleVarChange = (e, _variable, type) => {
const variable = cloneDeep(_variable);
switch (type) {
case 'name': {
variable.name = e.target.value;
break;
}
case 'value': {
variable.value = e.target.value;
break;
}
case 'enabled': {
variable.enabled = e.target.checked;
break;
}
}
reducerDispatch({
type: 'UPDATE_VAR',
variable
});
};
const handleRemoveVars = (variable) => {
reducerDispatch({
type: 'DELETE_VAR',
variable
});
};
return (
<StyledWrapper className="w-full mt-6 mb-6">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{variables && variables.length
? variables.map((variable, index) => {
return (
<tr key={variable.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={variable.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, variable, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={variable.value}
className="mousetrap"
onChange={(e) => handleVarChange(e, variable, 'value')}
/>
</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>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<div>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
+ Add Variable
</button>
</div>
<div>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" disabled={!hasChanges} onClick={saveChanges}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default EnvironmentVariables;

View File

@@ -0,0 +1,50 @@
import produce from 'immer';
import find from 'lodash/find';
import filter from 'lodash/filter';
import { uuid } from 'utils/common';
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_VAR': {
return produce(state, (draft) => {
draft.variables.push({
uid: uuid(),
name: '',
value: '',
type: 'text',
enabled: true
});
draft.hasChanges = true;
});
}
case 'UPDATE_VAR': {
return produce(state, (draft) => {
const variable = find(draft.variables, (v) => v.uid === action.variable.uid);
variable.name = action.variable.name;
variable.value = action.variable.value;
variable.enabled = action.variable.enabled;
draft.hasChanges = true;
});
}
case 'DELETE_VAR': {
return produce(state, (draft) => {
draft.variables = filter(draft.variables, (v) => v.uid !== action.variable.uid);
draft.hasChanges = true;
});
}
case 'CHANGES_SAVED': {
return produce(state, (draft) => {
draft.hasChanges = false;
});
}
default: {
return state;
}
}
};
export default reducer;

View File

@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { IconEdit, IconTrash, IconDatabase } from '@tabler/icons';
import EnvironmentVariables from './EnvironmentVariables';
import RenameEnvironment from '../../RenameEnvironment';
import DeleteEnvironment from '../../DeleteEnvironment';
const EnvironmentDetails = ({ environment, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
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} />}
<div className="flex">
<div className="flex flex-grow items-center">
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
<span className="ml-1 font-semibold">{environment.name}</span>
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
</div>
</div>
<div>
<EnvironmentVariables key={environment.uid} environment={environment} collection={collection} />
</div>
</div>
);
};
export default EnvironmentDetails;

View File

@@ -0,0 +1,52 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.collection.environment.settings.bg};
.environments-sidebar {
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
min-height: 400px;
}
.environment-item {
min-width: 150px;
display: block;
position: relative;
cursor: pointer;
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
}
}
.active {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
&:hover {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
}
}
.btn-create-environment {
padding: 8px 10px;
cursor: pointer;
border-bottom: none;
color: ${(props) => props.theme.textLink};
&:hover {
span {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,76 @@
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';
const EnvironmentList = ({ collection }) => {
const { environments } = collection;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
const envUids = environments ? environments.map((env) => env.uid) : [];
const prevEnvUids = usePrevious(envUids);
useEffect(() => {
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;
}
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
<div className="flex">
<div>
<div className="environments-sidebar">
{environments &&
environments.length &&
environments.map((env) => (
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span>
</div>
))}
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
+ <span>Create</span>
</div>
</div>
</div>
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
</div>
</StyledWrapper>
);
};
export default EnvironmentList;

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
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')
}),
onSubmit: (values) => {
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
.then(() => {
toast.success('Environment renamed successfully');
onClose();
})
.catch(() => toast.error('An error occured while renaming the environment'));
}
});
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
};
return (
<Portal>
<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">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
</Portal>
);
};
export default RenameEnvironment;

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
button.btn-create-environment {
&:hover {
span {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,34 @@
import Modal from 'components/Modal/index';
import React, { useState } from 'react';
import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
const EnvironmentSettings = ({ collection, onClose }) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
if (!environments || !environments.length) {
return (
<StyledWrapper>
<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)}>
+ <span>Create Environment</span>
</button>
</div>
</Modal>
</StyledWrapper>
);
}
return (
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList collection={collection} />
</Modal>
);
};
export default EnvironmentSettings;

View File

@@ -0,0 +1,17 @@
import React from 'react';
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>
);
}
export default SendIcon;

View File

@@ -1,11 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div`
&.modal--animate-out{
animation: fade-out 0.5s forwards cubic-bezier(.19,1,.22,1);
&.modal--animate-out {
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
.bruno-modal-card {
animation: fade-and-slide-out-from-top .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-out-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
}
@@ -23,8 +23,8 @@ const Wrapper = styled.div`
}
.bruno-modal-card {
animation-duration: .85s;
animation-delay: .1s;
animation-duration: 0.85s;
animation-delay: 0.1s;
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
@@ -33,28 +33,32 @@ const Wrapper = styled.div`
box-shadow: var(--box-shadow-base);
display: flex;
flex-direction: column;
will-change: opacity,transform;
will-change: opacity, transform;
flex-grow: 0;
margin: 3vh 10vw;
margin-top: 50px;
&.modal-sm {
min-width: 300px;
max-width: 500px;
}
&.modal-md {
min-width: 500px;
max-width: 800px;
}
&.modal-lg {
min-width: 800px;
max-width: 1140px;
}
&.modal-xl {
min-width: 1140px;
max-width: calc(100% - 30px);
}
animation: fade-and-slide-in-from-top .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-header {
@@ -62,8 +66,8 @@ const Wrapper = styled.div`
justify-content: space-between;
align-items: center;
text-transform: uppercase;
color: rgb(86 86 86);
background-color: #f1f1f1;
color: ${(props) => props.theme.modal.title.color};
background-color: ${(props) => props.theme.modal.title.bg};
font-size: 0.75rem;
padding: 12px;
font-weight: 600;
@@ -73,7 +77,7 @@ const Wrapper = styled.div`
.close {
font-size: 1.3rem;
line-height: 1;
color: #000;
color: ${(props) => props.theme.modal.iconColor};
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
margin-top: -2px;
@@ -86,7 +90,30 @@ const Wrapper = styled.div`
.bruno-modal-content {
flex-grow: 1;
background-color: #fff;
background-color: ${(props) => props.theme.modal.body.bg};
.textbox {
line-height: 1.42857143;
border: 1px solid #ccc;
padding: 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out .1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
.bruno-form {
color: ${(props) => props.theme.modal.body.color};
}
}
.bruno-modal-backdrop {
@@ -98,25 +125,32 @@ const Wrapper = styled.div`
will-change: opacity;
background: transparent;
&:before{
content: "";
&:before {
content: '';
height: 100%;
width: 100%;
left: 0;
opacity: .4;
opacity: ${(props) => props.theme.modal.backdrop.opacity};
top: 0;
background: black;
position: fixed;
}
animation: fade-in .1s forwards cubic-bezier(.19,1,.22,1);
animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-footer {
background-color: white;
background-color: ${(props) => props.theme.modal.body.bg};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&.modal-footer-none {
.bruno-modal-content {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
`;
export default Wrapper;

View File

@@ -1,7 +1,7 @@
import React, {useState, useEffect} from 'react';
import React, { useState, useEffect } from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({title, handleCancel}) => (
const ModalHeader = ({ title, handleCancel }) => (
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
{handleCancel ? (
@@ -12,42 +12,33 @@ const ModalHeader = ({title, handleCancel}) => (
</div>
);
const ModalContent = ({children}) => (
<div className="bruno-modal-content px-4 py-6">
{children}
</div>
);
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled}) => {
const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel, hideFooter }) => {
confirmText = confirmText || 'Save';
cancelText = cancelText || 'Cancel';
if (hideFooter) {
return null;
}
return (
<div className="flex justify-end p-4 bruno-modal-footer">
<span className="mr-2">
<span className={hideCancel ? 'hidden' : 'mr-2'}>
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
{cancelText}
</button>
</span>
<span className="">
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit} >
<span>
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit}>
{confirmText}
</button>
</span>
</div>
);
}
};
const Modal = ({
size,
title,
confirmText,
cancelText,
handleCancel,
handleConfirm,
children,
confirmDisabled
}) => {
const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfirm, children, confirmDisabled, hideCancel, hideFooter }) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
const escKeyCode = 27;
@@ -59,33 +50,40 @@ const Modal = ({
const closeModal = () => {
setIsClosing(true);
setTimeout(() => handleCancel(), 500);
}
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
}
};
}, []);
let classes = 'bruno-modal';
if (isClosing) {
classes += ' modal--animate-out';
}
if(hideFooter) {
classes += ' modal-footer-none';
}
return (
<StyledWrapper className={classes}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal()} />
<ModalContent>{children}</ModalContent>
<ModalFooter
<ModalFooter
confirmText={confirmText}
cancelText={cancelText}
handleCancel={() => closeModal()}
handleSubmit={handleConfirm}
handleCancel={() => closeModal()}
handleSubmit={handleConfirm}
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
hideFooter={hideFooter}
/>
</div>
{/* Clicking on backdrop closes the modal */}
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
</StyledWrapper>
);

View File

@@ -16,4 +16,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,7 +1,7 @@
import React, { useState, forwardRef, useRef } from 'react';
import Dropdown from '../Dropdown';
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconBox, IconSearch, IconDots } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
@@ -9,11 +9,11 @@ const Navbar = () => {
const [modalOpen, setModalOpen] = useState(false);
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => menuDropdownTippyRef.current = ref;
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
const MenuIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="dropdown-icon cursor-pointer">
<IconDots size={22}/>
<IconDots size={22} />
</div>
);
});
@@ -25,27 +25,36 @@ const Navbar = () => {
{/* <FontAwesomeIcon className="ml-2" icon={faCaretDown} style={{fontSize: 13}}/> */}
</div>
<div className="collection-dropdown flex flex-grow items-center justify-end">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement='bottom-start'>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}>
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}
>
Create Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}
>
Import Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}
>
Settings
</div>
</Dropdown>
</div>
</StyledWrapper>
)
);
};
export default Navbar;

View File

@@ -0,0 +1,8 @@
import { createPortal } from 'react-dom';
function Portal({ children, wrapperId }) {
wrapperId = wrapperId || 'bruno-app-body';
return createPortal(children, document.getElementById(wrapperId));
}
export default Portal;

View File

@@ -0,0 +1,68 @@
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,162 @@
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

@@ -5,37 +5,48 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
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;
}
}
}
.btn-add-param {
.btn-add-assertion {
font-size: 0.8125rem;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

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

@@ -0,0 +1,57 @@
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-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&: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,124 @@
import React from 'react';
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 { 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 = () => {
dispatch(
addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
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) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateFormUrlEncodedParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteFormUrlEncodedParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={param.value}
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')} />
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
</StyledWrapper>
);
};
export default FormUrlEncodedParams;

View File

@@ -10,17 +10,21 @@ const StyledWrapper = styled.div`
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -0,0 +1,175 @@
import React, { useEffect } from 'react';
import find from 'lodash/find';
import get from 'lodash/get';
import classnames from 'classnames';
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';
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
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();
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let {
schema,
loadSchema,
isLoading: isSchemaLoading,
error: schemaError
} = useGraphqlSchema(url, environment);
const loadGqlSchema = () => {
if(!isSchemaLoading) {
loadSchema();
}
};
useEffect(() => {
if(onSchemaLoad) {
onSchemaLoad(schema);
}
}, [schema]);
const onQueryChange = (value) => {
dispatch(
updateRequestGraphqlQuery({
query: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const selectTab = (tab) => {
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const getTabPanel = (tab) => {
switch (tab) {
case 'query': {
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>;
}
}
};
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === focusedTab.requestPaneTab
});
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<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={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>
</div>
</div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};
export default GraphQLRequestPane;

View File

@@ -0,0 +1,55 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { buildClientSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
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) {
return null;
}
return buildClientSchema(JSON.parse(saved));
} catch {
localStorage.setItem(localStorageKey, null);
return null;
}
});
const loadSchema = () => {
setIsLoading(true);
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');
} else {
return Promise.reject(new Error('An error occurred while introspecting schema'));
}
})
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error('Error occured while loading GraphQL Schema');
});
};
return {
isLoading,
schema,
loadSchema,
error
};
};
export default useGraphqlSchema;

View File

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

View File

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

@@ -10,17 +10,21 @@ const StyledWrapper = styled.div`
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
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 }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const selectTab = (tab) => {
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const getTabPanel = (tab) => {
switch (tab) {
case 'params': {
return <QueryParams item={item} collection={collection} />;
}
case 'body': {
return <RequestBody item={item} 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>;
}
}
};
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === focusedTab.requestPaneTab
});
};
return (
<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')}>
Query
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
</div>
<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' ? (
<div className="flex flex-grow justify-end items-center">
<RequestBodyMode item={item} collection={collection} />
</div>
) : null}
</div>
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};
export default HttpRequestPane;

View File

@@ -0,0 +1,58 @@
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-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&: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,124 @@
import React from 'react';
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 { 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 = () => {
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
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) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateMultipartFormParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteMultipartFormParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) => handleParamChange({
target: {
value: newValue
}
}, param, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<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>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
</StyledWrapper>
);
};
export default MultipartFormParams;

View File

@@ -0,0 +1,47 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
/* todo: find a better way */
height: calc(100vh - 220px);
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.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;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
color: #b5cea8 !important;
}
.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,100 +41,96 @@ 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,
minFoldSize: 4
},
lint: {
schema: this.props.schema,
validationRules: this.props.validationRules ?? null,
// linting accepts string or FragmentDefinitionNode[]
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
hintOptions: {
schema: this.props.schema,
closeOnUnfocus: false,
completeSingle: false,
container: this._node,
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
info: {
schema: this.props.schema,
renderDescription: (text) => md.render(text),
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference),
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
jump: {
schema: this.props.schema,
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference)
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: {
'Cmd-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Enter': () => {
if (this.props.onRunQuery) {
this.props.onRunQuery();
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onRunQuery) {
this.props.onRunQuery();
if (this.props.onRun) {
this.props.onRun();
}
},
'Shift-Ctrl-C': () => {
if (this.props.onCopyQuery) {
this.props.onCopyQuery();
}
},
'Shift-Ctrl-P': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();
}
},
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
'Shift-Ctrl-F': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();
}
},
'Shift-Ctrl-M': () => {
if (this.props.onMergeQuery) {
this.props.onMergeQuery();
}
},
'Cmd-S': () => {
if (this.props.onRunQuery) {
// empty
if (this.props.onSave) {
this.props.onSave();
return false;
}
},
'Ctrl-S': () => {
if (this.props.onRunQuery) {
// empty
if (this.props.onSave) {
this.props.onSave();
return false;
}
},
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent'
}
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -138,6 +138,7 @@ export default class QueryEditor extends React.Component {
editor.on('hasCompletion', this._onHasCompletion);
editor.on('beforeChange', this._onBeforeChange);
}
this.addOverlay();
}
componentDidUpdate(prevProps) {
@@ -152,14 +153,19 @@ export default class QueryEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
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;
}
@@ -172,20 +178,33 @@ 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
className="h-full"
className="h-full w-full"
aria-label="Query Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>
);
}
_onKeyUp = (_cm, event) => {
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
_onKeyUp = (_cm, e) => {
if (e.metaKey || e.ctrlKey || e.altKey) {
return;
}
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
this.editor.execCommand('autocomplete');
}
};

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2021 GraphQL Contributors.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import escapeHTML from 'escape-html';
import MD from 'markdown-it';
import { GraphQLNonNull, GraphQLList } from 'graphql';
const md = new MD();
/**
* Render a custom UI for CodeMirror's hint which includes additional info
* about the type and description for the selected context.
*/
export default function onHasCompletion(_cm, data, onHintInformationRender) {
const CodeMirror = require('codemirror');
let information;
let deprecation;
// When a hint result is selected, we augment the UI with information.
CodeMirror.on(data, 'select', (ctx, el) => {
// Only the first time (usually when the hint UI is first displayed)
// do we create the information nodes.
if (!information) {
const hintsUl = el.parentNode;
// This "information" node will contain the additional info about the
// highlighted typeahead option.
information = document.createElement('div');
information.className = 'CodeMirror-hint-information';
hintsUl.appendChild(information);
// This "deprecation" node will contain info about deprecated usage.
deprecation = document.createElement('div');
deprecation.className = 'CodeMirror-hint-deprecation';
hintsUl.appendChild(deprecation);
// When CodeMirror attempts to remove the hint UI, we detect that it was
// removed and in turn remove the information nodes.
let onRemoveFn;
hintsUl.addEventListener(
'DOMNodeRemoved',
(onRemoveFn = (event) => {
if (event.target === hintsUl) {
hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn);
information = null;
deprecation = null;
onRemoveFn = null;
}
})
);
}
// Now that the UI has been set up, add info to information.
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>';
if (ctx && deprecation && ctx.deprecationReason) {
const reason = ctx.deprecationReason ? md.render(ctx.deprecationReason) : '';
deprecation.innerHTML = '<span className="deprecation-label">Deprecated</span>' + reason;
deprecation.style.display = 'block';
} else if (deprecation) {
deprecation.style.display = 'none';
}
// Additional rendering?
if (onHintInformationRender) {
onHintInformationRender(information);
}
});
}
function renderType(type) {
if (type instanceof GraphQLNonNull) {
return `${renderType(type.ofType)}!`;
}
if (type instanceof GraphQLList) {
return `[${renderType(type.ofType)}]`;
}
return `<a className="typeName">${escapeHTML(type.name)}</a>`;
}

View File

@@ -0,0 +1,59 @@
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-param {
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;
}
}
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,127 @@
import React from 'react';
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 = () => {
dispatch(
addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
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) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateQueryParam({
param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParam = (param) => {
dispatch(
deleteQueryParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={param.value}
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')} />
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
+&nbsp;<span>Add Param</span>
</button>
</StyledWrapper>
);
};
export default QueryParams;

View File

@@ -17,7 +17,7 @@ const Wrapper = styled.div`
}
.dropdown-item {
padding: .25rem .6rem !important;
padding: 0.25rem 0.6rem !important;
}
}

View File

@@ -0,0 +1,53 @@
import React, { useRef, forwardRef } from 'react';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const HttpMethodSelector = ({ method, onMethodSelect }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
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>
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>
</div>
);
});
const handleMethodSelect = (verb) => onMethodSelect(verb);
const Verb = ({ verb }) => {
return (
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleMethodSelect(verb);
}}
>
{verb}
</div>
);
};
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer method-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-start">
<Verb verb="GET" />
<Verb verb="POST" />
<Verb verb="PUT" />
<Verb verb="DELETE" />
<Verb verb="PATCH" />
<Verb verb="OPTIONS" />
<Verb verb="HEAD" />
</Dropdown>
</div>
</StyledWrapper>
);
};
export default HttpMethodSelector;

View File

@@ -4,21 +4,18 @@ const Wrapper = styled.div`
height: 2.3rem;
div.method-selector-container {
border: solid 1px var(--color-layout-border);
border-right: none;
background-color: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
div.input-container {
border: solid 1px var(--color-layout-border);
background-color: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
input {
background-color: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
outline: none;
box-shadow: none;

View File

@@ -0,0 +1,75 @@
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, 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({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
})
);
};
const onMethodSelect = (verb) => {
dispatch(
updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="flex items-center">
<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"
style={{
color: 'yellow',
width: `calc(100% - ${methodSelectorWidth}px)`,
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
}}
>
<SingleLineEditor
value={url}
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}/>
</div>
</div>
</StyledWrapper>
);
};
export default QueryUrl;

View File

@@ -4,16 +4,16 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
.body-mode-selector {
background: #efefef;
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
border-radius: 3px;
.dropdown-item {
padding: .2rem .6rem !important;
padding: 0.2rem 0.6rem !important;
padding-left: 1.5rem !important;
}
.label-item {
padding: .2rem .6rem !important;
.label-item {
padding: 0.2rem 0.6rem !important;
}
}

View File

@@ -0,0 +1,100 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';
import { humanizeRequestBodyMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const RequestBodyMode = ({ item, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>
);
});
const onModeChange = (value) => {
dispatch(
updateRequestBodyMode({
itemUid: item.uid,
collectionUid: collection.uid,
mode: value
})
);
};
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer body-mode-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div className="label-item font-medium">Form</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('multipartForm');
}}
>
Multipart Form
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('formUrlEncoded');
}}
>
Form URL Encoded
</div>
<div className="label-item font-medium">Raw</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('json');
}}
>
JSON
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('xml');
}}
>
XML
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('text');
}}
>
TEXT
</div>
<div className="label-item font-medium">Other</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Body
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default RequestBodyMode;

View File

@@ -3,7 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
height: calc(100vh - 220px);
}
`;

View File

@@ -4,26 +4,33 @@ import CodeEditor from 'components/CodeEditor';
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
import { useDispatch } from 'react-redux';
import { updateRequestBody, sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const RequestBody = ({item, collection}) => {
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 onEdit = (value) => {
dispatch(updateRequestBody({
content: value,
itemUid: item.uid,
collectionUid: collection.uid,
}));
dispatch(
updateRequestBody({
content: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));;
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
if(['json', 'xml', 'text'].includes(bodyMode)) {
if (['json', 'xml', 'text'].includes(bodyMode)) {
let codeMirrorMode = {
json: 'application/ld+json',
text: 'application/text',
@@ -36,31 +43,21 @@ const RequestBody = ({item, collection}) => {
xml: body.xml
};
return(
return (
<StyledWrapper className="w-full">
<CodeEditor
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>
);
}
if(bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection}/>;
if (bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection} />;
}
if(bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection}/>;
if (bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection} />;
}
return(
<StyledWrapper className="w-full">
No Body
</StyledWrapper>
);
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;

View File

@@ -5,39 +5,48 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
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-header {
font-size: 0.8125rem;
margin-block: 10px;
padding: 5px;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,124 @@
import React from 'react';
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 = () => {
dispatch(
addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
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) {
case 'name': {
header.name = e.target.value;
break;
}
case 'value': {
header.value = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
}
}
dispatch(
updateRequestHeader({
header: header,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveHeader = (header) => {
dispatch(
deleteRequestHeader({
headerUid: header.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={header.value}
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')} />
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
</StyledWrapper>
);
};
export default RequestHeaders;

View File

@@ -0,0 +1,48 @@
import React, { useState, useEffect } from 'react';
import { faFolder } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import StyledWrapper from './StyledWrapper';
import Modal from 'components//Modal';
const SaveRequest = ({ items, onClose }) => {
const [showFolders, setShowFolders] = useState([]);
useEffect(() => {
setShowFolders(items || []);
}, [items]);
const handleFolderClick = (folder) => {
let subFolders = [];
if (folder.items && folder.items.length) {
for (let item of folder.items) {
if (item.items) {
subFolders.push(item);
}
}
if (subFolders.length) {
setShowFolders(subFolders);
}
}
};
return (
<StyledWrapper>
<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
? showFolders.map((folder) => (
<div key={folder.uid} className="folder-name" onClick={() => handleFolderClick(folder)}>
<FontAwesomeIcon className="mr-3 text-gray-500" icon={faFolder} style={{ fontSize: 20 }} />
{folder.name}
</div>
))
: null}
</div>
</Modal>
</StyledWrapper>
);
};
export default SaveRequest;

View File

@@ -2,13 +2,12 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
height: inherit;
}
textarea.cm-editor {
position: relative;
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

@@ -2,15 +2,9 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
/* todo: find a better way */
height: calc(100vh - 250px);
}
textarea.cm-editor {
position: relative;
height: calc(100vh - 220px);
}
`;
export default StyledWrapper;

View File

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

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

View File

@@ -5,37 +5,48 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
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-param {
.btn-add-var {
font-size: 0.8125rem;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,145 @@
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,56 @@
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

@@ -0,0 +1,44 @@
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(
closeTabs({
tabUids: [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 .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
</button>
</div>
);
};
export default RequestNotFound;

View File

@@ -0,0 +1,49 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
&.dragging {
cursor: col-resize;
}
div.drag-request {
display: flex;
align-items: center;
justify-content: center;
width: 10px;
padding: 0;
cursor: col-resize;
background: transparent;
div.drag-request-border {
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
}
&:hover div.drag-request-border {
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
div.graphql-docs-explorer-container {
background: white;
outline: none;
box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px;
position: absolute;
right: 0px;
z-index: 2000;
width: 350px;
height: 100%;
div.doc-explorer-title {
text-align: left;
}
div.doc-explorer-rhs {
display: flex;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,183 @@
import React, { useState, useEffect, useRef } from 'react';
import find from 'lodash/find';
import toast from 'react-hot-toast';
import { useSelector, useDispatch } from 'react-redux';
import GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';
import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
import ResponsePane from 'components/ResponsePane';
import Welcome from 'components/Welcome';
import { findItemInCollection } from 'utils/collections';
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
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 { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
const DEFAULT_PADDING = 5;
const RequestTabPanel = () => {
if (typeof window == 'undefined') {
return <div></div>;
}
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const collections = useSelector((state) => state.collections.collections);
const screenWidth = useSelector((state) => state.app.screenWidth);
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 [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
const [dragging, setDragging] = useState(false);
// Not a recommended pattern here to have the child component
// make a callback to set state, but treating this as an exception
const docExplorerRef = useRef(null);
const [schema, setSchema] = useState(null);
const [showGqlDocs, setShowGqlDocs] = useState(false);
const onSchemaLoad = (schema) => setSchema(schema);
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
const handleGqlClickReference = (reference) => {
if(docExplorerRef.current) {
docExplorerRef.current.showDocForReference(reference);
}
if(!showGqlDocs) {
setShowGqlDocs(true);
}
};
useEffect(() => {
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
useEffect(() => {
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if (dragging) {
e.preventDefault();
let leftPaneXPosition = e.clientX + 2;
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
return;
}
setLeftPaneWidth(leftPaneXPosition- asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
}
};
const handleMouseUp = (e) => {
if (dragging) {
e.preventDefault();
setDragging(false);
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
}
};
const handleDragbarMouseDown = (e) => {
e.preventDefault();
setDragging(true);
};
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging, asideWidth]);
if (!activeTabUid) {
return <Welcome />;
}
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
if (!collection || !collection.uid) {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const showRunner = collection.showRunner;
if(showRunner) {
return <RunnerResults collection={collection}/>;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
}
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
);
};
return (
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
<div className="pt-4 pb-3 px-4">
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
</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)` }}>
{item.type === 'graphql-request' ? (
<GraphQLRequestPane
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
/>
) : null}
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} /> : null}
</div>
</section>
<div className="drag-request" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
<section className="response-pane flex-grow">
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
</section>
</section>
{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"
>
{'\u2715'}
</button>
</DocExplorer>
</div>
): null}
</StyledWrapper>
);
};
export default RequestTabPanel;

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { IconFiles, IconRun } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import VariablesView from 'components/VariablesView';
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
}));
};
return (
<StyledWrapper>
<div className="flex items-center p-2">
<div className="flex flex-1 items-center">
<IconFiles size={18} strokeWidth={1.5} />
<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>
<VariablesView collection={collection}/>
<EnvironmentSelector collection={collection} />
</div>
</div>
</StyledWrapper>
);
};
export default CollectionToolBar;

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

@@ -8,31 +8,33 @@ const StyledWrapper = styled.div`
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: nowrap;
}
.close-icon-container {
min-height: 20px;
min-width: 24px;
margin-left: 4px;
border-radius: 3px;
.close-icon {
display: none;
color: #9f9f9f;
color: ${(props) => props.theme.requestTabs.icon.color};
width: 8px;
padding-bottom: 6px;
padding-top: 6px;
}
&:hover, &:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
&:hover,
&:hover .close-icon {
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
}
.has-changes-icon {
.has-changes-icon {
height: 24px;
}
}
`;
export default StyledWrapper;

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