Compare commits

..

182 Commits

Author SHA1 Message Date
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
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
292 changed files with 10950 additions and 4652 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

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

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

4
.gitignore vendored
View File

@@ -17,6 +17,7 @@ chrome-extension
chrome-extension.pem
chrome-extension.crx
bruno.zip
*.zip
# misc
.DS_Store
@@ -37,3 +38,6 @@ yarn-error.log*
/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

View File

@@ -1,28 +1,31 @@
## 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 computed.
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
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 and npm 8.x. We use npm workspaces in the project
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
```bash
# clone and cd into bruno
# use Node 14.x, Npm 8.x
# use Node 14.x, Npm 8.x
# Install deps (note that we use npm workspaces)
npm i
@@ -41,4 +44,10 @@ open http://localhost:3000
```
### Raising Pull Request
* Please keep the PR's small and focused on one thing
- 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,6 +1,16 @@
## 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
###
```bash
# use nodejs 18 version
nvm use
# install deps
npm i
@@ -14,14 +24,17 @@ npm run dev --workspace=packages/bruno-electron
npm run build --workspace=packages/bruno-app
```
## fix
### fix
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
# testing
### testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-schema
```

View File

@@ -1,55 +0,0 @@
const { ipcMain } = require('electron');
const template = [
{
label: 'Collection',
submenu: [
{
label: 'Open Collection',
click () {
ipcMain.emit('main:open-collection');
}
},
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo'},
{ role: 'redo'},
{ role: 'separator'},
{ role: 'cut'},
{ role: 'copy'},
{ role: 'paste'}
]
},
{
label: 'View',
submenu: [
{ role: 'reload'},
{ role: 'toggledevtools'},
{ role: 'separator'},
{ role: 'resetzoom'},
{ role: 'zoomin'},
{ role: 'zoomout'},
{ role: 'separator'},
{ role: 'togglefullscreen'}
]
},
{
role: 'window',
submenu: [
{ role: 'minimize'},
{ role: 'close'}
]
},
{
role: 'help',
submenu: [
{ label: 'Learn More'}
]
}
];
module.exports = template;

View File

@@ -1,54 +0,0 @@
const path = require('path');
const { format } = require('url');
const { BrowserWindow, app, Menu } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
const menuTemplate = require('./app/menu-template');
const registerIpc = require('./ipc');
const isDev = require('electron-is-dev');
const prepareNext = require('electron-next');
setContentSecurityPolicy(`
default-src * 'unsafe-inline' 'unsafe-eval';
script-src * 'unsafe-inline' 'unsafe-eval';
connect-src * 'unsafe-inline';
base-uri 'none';
form-action 'none';
frame-ancestors 'none';
`);
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
let mainWindow;
// Prepare the renderer once the app is ready
app.on('ready', async () => {
await prepareNext('./renderer');
mainWindow = new BrowserWindow({
width: 1280,
height: 768,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, "preload.js")
},
});
const url = isDev
? 'http://localhost:8000'
: format({
pathname: path.join(__dirname, '../renderer/out/index.html'),
protocol: 'file:',
slashes: true
});
mainWindow.loadURL(url);
// register all ipc handlers
registerIpc(mainWindow);
});
// Quit the app once all windows are closed
app.on('window-all-closed', app.quit);

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;

View File

@@ -1,14 +0,0 @@
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('ipcRenderer', {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
on: (channel, handler) => {
// Deliberately strip event as it includes `sender`
const subscription = (event, ...args) => handler(...args);
ipcRenderer.on(channel, subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
};
}
});

View File

@@ -1,14 +0,0 @@
const { customAlphabet } = require('nanoid');
// a customized version of nanoid without using _ and -
const uuid = () => {
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
const customNanoId = customAlphabet (urlAlphabet, 21);
return customNanoId();
};
module.exports = {
uuid
};

View File

@@ -6,17 +6,29 @@
"packages/bruno-electron",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-testbench"
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@playwright/test": "^1.27.1",
"jest": "^29.2.0",
"randomstring": "^1.2.2"
},
"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:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
"build:electron": "./scripts/build-electron.sh"
"build:electron": "./scripts/build-electron.sh",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report"
},
"overrides": {
"rollup": "3.2.5"
}
}

View File

@@ -6,6 +6,8 @@
"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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 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

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

View File

@@ -1,8 +1,7 @@
import React from 'react';
import Modal from 'components/Modal/index';
import { IconSpeakerphone, IconBrandTwitter } from '@tabler/icons';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import GithubSvg from 'assets/github.svg';
const BrunoSupport = ({ onClose }) => {
return (
@@ -10,19 +9,25 @@ const BrunoSupport = ({ onClose }) => {
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
<div className="collection-options">
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-center">
<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://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<img src={GithubSvg.src} style={{ width: '18px' }} />
<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-center">
<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>

View File

@@ -2,12 +2,44 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
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() {
@@ -38,7 +42,9 @@ export default class QueryEditor extends React.Component {
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly ? 'nocursor' : false,
readOnly: this.props.readOnly,
scrollbarStyle: "overlay",
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-Enter': () => {
if (this.props.onRun) {
@@ -60,6 +66,8 @@ export default class QueryEditor extends React.Component {
this.props.onSave();
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
}
@@ -67,6 +75,7 @@ export default class QueryEditor extends React.Component {
}));
if (editor) {
editor.on('change', this._onEdit);
this.addOverlay();
}
}
@@ -85,7 +94,17 @@ export default class QueryEditor extends React.Component {
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
this.editor.setOption('mode', this.props.mode);
}
if(this.editor) {
let variables = getEnvironmentVariables(this.props.collection);
if (!isEqual(variables, this.variables)) {
this.addOverlay();
}
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
this.ignoreChangeEvent = false;
}
@@ -109,6 +128,15 @@ export default class QueryEditor extends React.Component {
);
}
addOverlay = () => {
const mode = this.props.mode || 'application/ld+json';
let variables = getEnvironmentVariables(this.props.collection);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode);
this.editor.setOption('mode', 'brunovariables');
}
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();

View File

@@ -9,11 +9,11 @@ const Wrapper = styled.div`
.tippy-box {
min-width: 135px;
background-color: white;
font-size: 0.8125rem;
color: rgb(48 48 48);
background: #fff;
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px;
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;
@@ -25,6 +25,7 @@ const Wrapper = styled.div`
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
background-color: ${(props) => props.theme.dropdown.labelBg};
}
.dropdown-item {
@@ -33,8 +34,16 @@ const Wrapper = styled.div`
padding: 0.35rem 0.6rem;
cursor: pointer;
.icon {
color: ${(props) => props.theme.dropdown.iconColor};
}
&:hover {
background-color: #e9e9e9;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
}
}
}

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

@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
@@ -63,13 +63,14 @@ const EnvironmentSelector = ({ collection }) => {
onSelect(null);
}}
>
<span>No Environment</span>
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className='ml-2'>No Environment</span>
</div>
<div className="dropdown-item" style={{ borderTop: 'solid 1px #e7e7e7' }} onClick={() => setOpenSettingsModal(true)}>
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Settings</span>
<span>Configure</span>
</div>
</Dropdown>
</div>

View File

@@ -8,11 +8,11 @@ const Wrapper = styled.div`
thead,
td {
border: 1px solid #efefef;
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;
}
@@ -29,6 +29,7 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: transparent;
&:focus {
outline: none !important;

View File

@@ -7,7 +7,6 @@ import DeleteEnvironment from '../../DeleteEnvironment';
const EnvironmentDetails = ({ environment, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
console.log(environment);
return (
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>

View File

@@ -4,8 +4,11 @@ const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.collection.environment.settings.bg};
.environments-sidebar {
background-color: #eaeaea;
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;
}
@@ -20,15 +23,15 @@ const StyledWrapper = styled.div`
&:hover {
text-decoration: none;
background-color: #e4e4e4;
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
}
}
.active {
background-color: #dcdcdc !important;
border-left: solid 2px var(--color-brand);
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: #dcdcdc !important;
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
}
}
@@ -36,7 +39,7 @@ const StyledWrapper = styled.div`
padding: 8px 10px;
cursor: pointer;
border-bottom: none;
color: var(--color-text-link);
color: ${(props) => props.theme.textLink};
&:hover {
span {

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react';
import { findEnvironmentInCollection } from 'utils/collections';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment/index';
import StyledWrapper from './StyledWrapper';
@@ -9,8 +10,13 @@ const EnvironmentList = ({ collection }) => {
const [openCreateModal, setOpenCreateModal] = useState(false);
useEffect(() => {
setSelectedEnvironment(environments[0]);
}, []);
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if(environment) {
setSelectedEnvironment(environment);
} else {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}
}, [collection, environments]);
if (!selectedEnvironment) {
return null;

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

@@ -66,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;
@@ -77,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;
@@ -90,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 {
@@ -107,7 +130,7 @@ const Wrapper = styled.div`
height: 100%;
width: 100%;
left: 0;
opacity: 0.4;
opacity: ${(props) => props.theme.modal.backdrop.opacity};
top: 0;
background: black;
position: fixed;
@@ -117,10 +140,17 @@ const Wrapper = styled.div`
}
.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

@@ -64,6 +64,9 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
if (isClosing) {
classes += ' modal--animate-out';
}
if(hideFooter) {
classes += ' modal-footer-none';
}
return (
<StyledWrapper className={classes}>
<div className={`bruno-modal-card modal-${size}`}>

View File

@@ -5,19 +5,28 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid #efefef;
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;
}
}
}
@@ -29,6 +38,8 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

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

View File

@@ -1,22 +1,14 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.react-tabs__tab-list {
border-bottom: none !important;
padding-top: 0;
padding-left: 0 !important;
display: flex;
align-items: center;
margin: 0;
.react-tabs__tab {
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
user-select: none;
border-bottom: solid 2px transparent;
margin-right: 20px;
color: rgb(125 125 125);
outline: none !important;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
@@ -27,36 +19,12 @@ const StyledWrapper = styled.div`
box-shadow: none !important;
}
&:after {
display: none !important;
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
.react-tabs__tab--selected {
border: none;
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
border: none;
outline: none !important;
box-shadow: none !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -1,26 +1,159 @@
import React from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
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 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 = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
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 '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="h-full">
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
<TabList>
<Tab tabIndex="-1">Query</Tab>
<Tab tabIndex="-1">Headers</Tab>
</TabList>
<TabPanel>
<div className="mt-4">
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
<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('script')} role="tab" onClick={() => selectTab('script')}>
Script
</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 ? 'Schema' : 'Load Schema'}</span>
</div>
</TabPanel>
<TabPanel>
<RequestHeaders />
</TabPanel>
</Tabs>
<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>
);
};

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

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

View File

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

@@ -20,8 +20,8 @@ const StyledWrapper = styled.div`
}
&.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;
}
}
}

View File

@@ -7,6 +7,8 @@ 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 Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
@@ -34,6 +36,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
case 'headers': {
return <RequestHeaders 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>;
}
@@ -67,6 +75,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</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' ? (

View File

@@ -5,19 +5,28 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid #efefef;
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;
}
}
}
@@ -29,6 +38,9 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

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

View File

@@ -2,14 +2,46 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
/* todo: find a better way */
height: calc(100vh - 250px);
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,11 +41,17 @@ export default class QueryEditor extends React.Component {
lineNumbers: true,
tabSize: 2,
mode: 'graphql',
// mode: 'brunovariables',
brunoVarInfo: {
variables: getAllVariables(this.props.collection),
},
theme: this.props.editorTheme || 'graphiql',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
scrollbarStyle: "overlay",
readOnly: this.props.readOnly ? 'nocursor' : false,
foldGutter: {
minFoldSize: 4
@@ -75,54 +85,51 @@ export default class QueryEditor extends React.Component {
'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) {
@@ -131,6 +138,7 @@ export default class QueryEditor extends React.Component {
editor.on('hasCompletion', this._onHasCompletion);
editor.on('beforeChange', this._onBeforeChange);
}
this.addOverlay();
}
componentDidUpdate(prevProps) {
@@ -149,6 +157,15 @@ export default class QueryEditor extends React.Component {
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;
}
@@ -161,10 +178,20 @@ 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) => {
this._node = node;
@@ -173,8 +200,11 @@ export default class QueryEditor extends React.Component {
);
}
_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

@@ -5,19 +5,28 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid #efefef;
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;
}
}
}
@@ -32,6 +41,7 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;

View File

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

View File

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

View File

@@ -4,18 +4,18 @@ const Wrapper = styled.div`
height: 2.3rem;
div.method-selector-container {
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 {
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

@@ -1,16 +1,28 @@
import React from 'react';
import React, { useState, useEffect} from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import SendSvg from 'assets/send.svg';
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');
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
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({
@@ -36,19 +48,24 @@ const QueryUrl = ({ item, collection, handleRun }) => {
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full mousetrap"
type="text"
value={url}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={(event) => onUrlChange(event.target.value)}
<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" onClick={handleRun}>
<img src={SendSvg.src} style={{ width: '22px' }} />
<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>

View File

@@ -4,7 +4,7 @@ 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 {

View File

@@ -4,6 +4,7 @@ import CodeEditor from 'components/CodeEditor';
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
import { useDispatch } from 'react-redux';
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';
@@ -12,6 +13,9 @@ 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(
@@ -41,7 +45,7 @@ const RequestBody = ({ item, collection }) => {
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>
);
}

View File

@@ -5,32 +5,40 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid #efefef;
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'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;

View File

@@ -3,11 +3,15 @@ import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
@@ -19,6 +23,8 @@ const RequestHeaders = ({ item, collection }) => {
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
@@ -30,10 +36,6 @@ const RequestHeaders = ({ item, collection }) => {
header.value = e.target.value;
break;
}
case 'description': {
header.description = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
@@ -65,7 +67,6 @@ const RequestHeaders = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
@@ -87,27 +88,17 @@ const RequestHeaders = ({ item, collection }) => {
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleHeaderValueChange({
target: {
value: newValue
}
}, header, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
@@ -124,7 +115,7 @@ const RequestHeaders = ({ item, collection }) => {
: null}
</tbody>
</table>
<button className="btn-add-header select-none" onClick={addHeader}>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
</StyledWrapper>

View File

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

View File

@@ -0,0 +1,45 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestScript } 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 script = item.draft ? get(item, 'draft.request.script') : get(item, 'request.script');
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestScript({
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">
<CodeEditor
collection={collection} value={script || ''}
theme={storedTheme}
onEdit={onEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default Script;

View File

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

View File

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

View File

@@ -18,11 +18,30 @@ const StyledWrapper = styled.div`
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px var(--color-request-dragbar-background);
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
}
&:hover div.drag-request-border {
border-left: solid 1px var(--color-request-dragbar-background-active);
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;
}
}
`;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import find from 'lodash/find';
import toast from 'react-hot-toast';
import { useSelector, useDispatch } from 'react-redux';
@@ -12,10 +12,15 @@ 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 useGraphqlSchema from '../../hooks/useGraphqlSchema';
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>;
@@ -29,23 +34,43 @@ const RequestTabPanel = () => {
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
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 - 5);
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if (dragging) {
e.preventDefault();
setLeftPaneWidth(e.clientX - asideWidth - 5);
setRightPaneWidth(screenWidth - e.clientX - 5);
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) => {
@@ -55,7 +80,7 @@ const RequestTabPanel = () => {
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
}
@@ -65,11 +90,6 @@ const RequestTabPanel = () => {
setDragging(true);
};
let schema = null;
// let {
// schema
// } = useGraphqlSchema('https://api.spacex.land/graphql');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
@@ -93,6 +113,11 @@ const RequestTabPanel = () => {
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} />;
@@ -105,24 +130,23 @@ const RequestTabPanel = () => {
})
);
};
const onGraphqlQueryChange = (value) => {};
const runQuery = async () => {};
return (
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
<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">
<section className="main flex flex-grow pb-4 relative">
<section className="request-pane">
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
<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
onRunQuery={runQuery}
schema={schema}
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
value={item.request.body.graphql.query}
onQueryChange={onGraphqlQueryChange}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
/>
) : null}
@@ -138,8 +162,22 @@ const RequestTabPanel = () => {
<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;
export default RequestTabPanel;

View File

@@ -1,9 +1,20 @@
import React from 'react';
import { IconFiles } from '@tabler/icons';
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">
@@ -12,6 +23,10 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>
<VariablesView collection={collection}/>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@@ -0,0 +1,42 @@
import React, { useState, useEffect } from 'react';
import { IconAlertTriangle } from '@tabler/icons';
const RequestTabNotFound = ({handleCloseClick}) => {
const [showErrorMessage, setShowErrorMessage] = useState(false);
// add a delay component in react that shows a loading spinner
// and then shows the error message after a delay
// this will prevent the error message from flashing on the screen
useEffect(() => {
setTimeout(() => {
setShowErrorMessage(true);
}, 300);
}, []);
if(!showErrorMessage) {
return null;
}
return (
<>
<div className="flex items-center tab-label pl-2">
{showErrorMessage ? (
<>
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</>
) : null}
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
</>
);
};
export default RequestTabNotFound;

View File

@@ -19,7 +19,7 @@ const StyledWrapper = styled.div`
.close-icon {
display: none;
color: #9f9f9f;
color: ${(props) => props.theme.requestTabs.icon.color};
width: 8px;
padding-bottom: 6px;
padding-top: 6px;
@@ -27,8 +27,8 @@ const StyledWrapper = styled.div`
&:hover,
&:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
}
.has-changes-icon {

View File

@@ -4,7 +4,7 @@ import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { IconAlertTriangle } from '@tabler/icons';
import RequestTabNotFound from './RequestTabNotFound';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
@@ -61,18 +61,7 @@ const RequestTab = ({ tab, collection }) => {
if (!item) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
<RequestTabNotFound handleCloseClick={handleCloseClick} />
</StyledWrapper>
);
}
@@ -85,7 +74,7 @@ const RequestTab = ({ tab, collection }) => {
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>
<span className="text-gray-700 ml-1 tab-name" title={item.name}>
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
</div>

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
border-bottom: 1px solid var(--color-request-dragbar-background);
border-bottom: 1px solid ${(props) => props.theme.requestTabs.borromBorder};
ul {
padding: 0;
@@ -28,7 +28,8 @@ const Wrapper = styled.div`
height: 38px;
margin-right: 6px;
background: #f7f7f7;
color: ${(props) => props.theme.requestTabs.color};
background: ${(props) => props.theme.requestTabs.bg};
border-radius: 0;
.tab-container {
@@ -36,7 +37,7 @@ const Wrapper = styled.div`
}
&.active {
background: #e7e7e7;
background: ${(props) => props.theme.requestTabs.active.bg};
font-weight: 500;
}
@@ -60,9 +61,9 @@ const Wrapper = styled.div`
padding: 3px 0px;
display: inline-flex;
justify-content: center;
color: rgb(117 117 117);
color: ${(props) => props.theme.requestTabs.shortTab.color};
background-color: ${(props) => props.theme.requestTabs.shortTab.bg};
position: relative;
background-color: white;
top: -1px;
> div {
@@ -85,8 +86,8 @@ const Wrapper = styled.div`
&:hover {
> div {
background-color: #eaeaea;
color: rgb(76 76 76);
background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg};
color: ${(props) => props.theme.requestTabs.shortTab.hoverColor};
border-radius: 3px;
}
}

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
import find from 'lodash/find';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { focusTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -76,6 +76,8 @@ const RequestTabs = () => {
});
};
const showRunner = activeCollection && activeCollection.showRunner;
// Todo: Must support ephermal requests
return (
<StyledWrapper className={getRootClassname()}>
@@ -83,59 +85,61 @@ const RequestTabs = () => {
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection} />
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<IconChevronLeft size={18} strokeWidth={1.5} />
{!showRunner ? (
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li>
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
</li>
);
})
: null}
</ul>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5} />
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
</div>
</li>
) : null}
<li className="select-none short-tab" onClick={createNewTab}>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
</div>
</li>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</div>
</li> */}
</ul>
</div>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</div>
</li> */}
</ul>
</div>
) : null}
</>
) : null}
</StyledWrapper>

View File

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

View File

@@ -23,7 +23,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
<IconRefresh size={24} className="animate-spin" />
<button
onClick={handleCancelRequest}
className="mt-4 uppercase bg-gray-200 active:bg-blueGray-600 text-xs px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
className="mt-4 uppercase btn-md rounded btn-secondary ease-linear transition-all duration-150"
type="button"
>
Cancel Request

View File

@@ -3,6 +3,10 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
padding-top: 20%;
width: 100%;
.send-icon {
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
}
`;
export default StyledWrapper;

View File

@@ -5,7 +5,7 @@ import StyledWrapper from './StyledWrapper';
const Placeholder = () => {
return (
<StyledWrapper>
<div className="text-gray-300 flex justify-center" style={{ fontSize: 200 }}>
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
<IconSend size={150} strokeWidth={1} />
</div>
<div className="flex mt-4">

View File

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

View File

@@ -22,7 +22,7 @@ const Wrapper = styled.div`
tbody {
tr:nth-child(odd) {
background-color: var(--color-table-stripe);
background-color: ${(props) => props.theme.table.striped};
}
}
}

View File

@@ -3,7 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.75rem;
font-weight: 600;
color: rgb(117 117 117);
color: ${(props) => props.theme.requestTabPanel.responseStatus};
`;
export default Wrapper;

View File

@@ -3,7 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.75rem;
font-weight: 600;
color: rgb(117 117 117);
color: ${(props) => props.theme.requestTabPanel.responseStatus};
`;
export default Wrapper;

View File

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

View File

@@ -3,19 +3,20 @@ import classnames from 'classnames';
import statusCodePhraseMap from './get-status-code-phrase';
import StyledWrapper from './StyledWrapper';
// Todo: text-error class is not getting pulled in for 500 errors
const StatusCode = ({ status }) => {
const getTabClassname = () => {
const getTabClassname = (status) => {
return classnames('', {
'text-blue-700': status >= 100 && status < 200,
'text-green-700': status >= 200 && status < 300,
'text-purple-700': status >= 300 && status < 400,
'text-yellow-700': status >= 400 && status < 500,
'text-red-700': status >= 500 && status < 600
'text-ok': status >= 100 && status < 200,
'text-ok': status >= 200 && status < 300,
'text-error': status >= 300 && status < 400,
'text-error': status >= 400 && status < 500,
'text-error': status >= 500 && status < 600
});
};
return (
<StyledWrapper className={getTabClassname()}>
<StyledWrapper className={getTabClassname(status)}>
{status} {statusCodePhraseMap[status]}
</StyledWrapper>
);

View File

@@ -20,11 +20,19 @@ const StyledWrapper = styled.div`
}
&.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;
}
}
}
.some-tests-failed {
color: ${(props) => props.theme.colors.text.danger} !important;
}
.all-tests-passed {
color: ${(props) => props.theme.colors.text.green} !important;
}
`;
export default StyledWrapper;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,140 @@
import React, { useState, useEffect } from 'react';
import path from 'path';
import { get, each, cloneDeep } from 'lodash';
import { findItemInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
const getRelativePath = (fullPath, pathname) => {
let relativePath = path.relative(fullPath, pathname);
const { dir, name } = path.parse(relativePath);
return path.join(dir, name);
}
export default function RunnerResults({collection}) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!collection.runnerResult) {
setSelectedItem(null);
}
}, [collection, setSelectedItem]);
const collectionCopy = cloneDeep(collection);
const items = cloneDeep(get(collection, 'runnerResult.items', []));
each(items, (item) => {
const info = findItemInCollection(collectionCopy, item.uid);
item.name = info.name;
item.type = info.type;
item.filename = info.filename;
item.pathname = info.pathname;
item.relativePath = getRelativePath(collection.pathname, info.pathname);
if(item.status !== "error") {
if(item.testResults) {
const failed = item.testResults.filter((result) => result.status === 'fail');
item.testStatus = failed.length ? 'fail' : 'pass';
} else {
item.testStatus = 'pass';
}
}
});
const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass');
const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail');
return (
<StyledWrapper className='px-4'>
<div className='font-medium mt-6 mb-4 title flex items-center'>
Runner
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
</div>
<div className='flex'>
<div className='flex flex-col flex-1'>
<div className="py-2 font-medium test-summary">
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
</div>
{items.map((item) => {
return (
<div key={item.uid}>
<div className="item-path mt-2">
<div className="flex items-center">
<span>
{item.status !== "error" && item.testStatus === 'pass' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
) : (
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
)}
</span>
<span className={`mr-1 ml-2 ${(item.status == "error" || item.testStatus == 'fail') ? 'danger' : ''}`}>{item.relativePath}</span>
{(item.status !== "error" && item.status !== "completed") ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5}/>
) : (
<span className='text-xs link cursor-pointer' onClick={() => setSelectedItem(item)}>
(<span className='mr-1'>
{get(item.responseReceived, 'status')}
</span>
<span>
{get(item.responseReceived, 'statusText')}
</span>)
</span>
)}
</div>
{item.status == "error" ? (
<div className="error-message pl-8 pt-2 text-xs">
{item.error}
</div>
) : null }
<ul className="pl-8">
{item.testResults ? item.testResults.map((result) => (
<li key={result.uid} className="py-1">
{result.status === 'pass' ? (
<span className="test-success flex items-center">
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
{result.description}
</span>
) : (
<>
<span className="test-failure flex items-center">
<IconX size={18} strokeWidth={2} className="mr-2"/>
{result.description}
</span>
<span className="error-message pl-8 text-xs">
{result.error}
</span>
</>
)}
</li>
)): null}
</ul>
</div>
</div>
);
})}
</div>
<div className='flex flex-1' style={{width: '50%'}}>
{selectedItem ? (
<div className='flex flex-col w-full overflow-auto'>
<div className="flex items-center px-3 mb-4 font-medium">
<span className='mr-2'>{selectedItem.relativePath}</span>
<span>
{selectedItem.testStatus === 'pass' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
) : (
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
)}
</span>
</div>
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
<ResponsePane item={selectedItem} collection={collection}/>
</div>
) : null}
</div>
</div>
</StyledWrapper>
);
};

View File

@@ -13,25 +13,25 @@ const Wrapper = styled.div`
}
.method-get {
color: var(--color-method-get);
color: ${(props) => props.theme.request.methods.get};
}
.method-post {
color: var(--color-method-post);
color: ${(props) => props.theme.request.methods.post};
}
.method-put {
color: var(--color-method-put);
color: ${(props) => props.theme.request.methods.put};
}
.method-delete {
color: var(--color-method-delete);
color: ${(props) => props.theme.request.methods.delete};
}
.method-patch {
color: var(--color-method-patch);
color: ${(props) => props.theme.request.methods.put};
}
.method-options {
color: var(--color-method-options);
color: ${(props) => props.theme.request.methods.put};
}
.method-head {
color: var(--color-method-head);
color: ${(props) => props.theme.request.methods.put};
}
`;

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.bruno-modal-content {
padding-bottom: 1rem;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import get from 'lodash/get';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const RunCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const onSubmit = (recursive) => {
dispatch(showRunnerView({
collectionUid: collection.uid,
}));
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
onClose();
};
const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
const items = flattenItems(item ? item.items : collection.items);
const requestItems = items.filter((item) => item.type !== 'folder');
const recursiveRunLength = requestItems.length;
return (
<StyledWrapper>
<Modal size="md" title='Collection Runner' hideFooter={true} handleCancel={onClose}>
<div className='mb-1'>
<span className='font-medium'>Run</span>
<span className='ml-1 text-xs'>({runLength} requests)</span>
</div>
<div className='mb-8'>
This will only run the requests in this folder.
</div>
<div className='mb-1'>
<span className='font-medium'>Recursive Run</span>
<span className='ml-1 text-xs'>({recursiveRunLength} requests)</span>
</div>
<div className='mb-8'>
This will run all the requests in this folder and all its subfolders.
</div>
<div className="flex justify-end bruno-modal-footer">
<span className='mr-3'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
Recursive Run
</button>
</span>
<span>
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
Run
</button>
</span>
</div>
</Modal>
</StyledWrapper>
);
};
export default RunCollectionItem;

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.menu-icon {
color: rgb(110 110 110);
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
.dropdown {
div[aria-expanded='true'] {
@@ -15,7 +15,7 @@ const Wrapper = styled.div`
}
.indent-block {
border-right: solid 1px #e1e1e1;
border-right: ${(props) => props.theme.sidebar.collection.item.indentBorder};
}
.collection-item-name {
@@ -34,7 +34,7 @@ const Wrapper = styled.div`
}
&:hover {
background: #e7e7e7;
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.menu-icon {
.dropdown {
div[aria-expanded='false'] {
@@ -45,14 +45,14 @@ const Wrapper = styled.div`
}
&.item-focused-in-tab {
background: var(--color-sidebar-collection-item-active-background);
background: ${(props) => props.theme.sidebar.collection.item.bg};
&:hover {
background: var(--color-sidebar-collection-item-active-background) !important;
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
}
.indent-block {
border-right: solid 1px var(--color-sidebar-collection-item-active-indent-border);
border-right: ${(props) => props.theme.sidebar.collection.item.active.indentBorder} !important;
}
}
@@ -62,15 +62,15 @@ const Wrapper = styled.div`
}
div.dropdown-item.delete-item {
color: var(--color-text-danger);
color: ${(props) => props.theme.colors.danger};
&:hover {
background-color: var(--color-background-danger);
background-color: ${(props) => props.theme.colors.bg.danger};
color: white;
}
}
}
&.is-dragging .collection-item-name {
&.is-sidebar-dragging .collection-item-name {
cursor: inherit;
}
`;

View File

@@ -2,10 +2,12 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
import range from 'lodash/range';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@@ -13,8 +15,10 @@ import RequestMethod from './RequestMethod';
import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
import RunCollectionItem from './RunCollectionItem';
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
import { getDefaultRequestPaneTab } from 'utils/collections';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
@@ -22,7 +26,7 @@ import StyledWrapper from './StyledWrapper';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isDragging = useSelector((state) => state.app.isDragging);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
@@ -30,8 +34,32 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
const [{ isDragging }, drag] = useDrag({
type: `COLLECTION_ITEM_${collection.uid}`,
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
});
const [{ isOver }, drop] = useDrop({
accept: `COLLECTION_ITEM_${collection.uid}`,
drop: (draggedItem) => {
if (draggedItem.uid !== item.uid) {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
}
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
});
useEffect(() => {
if (searchText && searchText.length) {
setItemisCollapsed(false);
@@ -58,6 +86,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
const handleClick = (event) => {
dispatch(hideRunnerView({
collectionUid: collection.uid
}));
if (isItemARequest(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
@@ -69,7 +100,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
}
@@ -89,7 +121,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const isFolder = isItemAFolder(item);
const className = classnames('flex flex-col w-full', {
'is-dragging': isDragging
'is-sidebar-dragging': isSidebarDragging
});
if (searchText && searchText.length) {
@@ -104,8 +136,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
}
const requestItems = filter(item.items, (i) => isItemARequest(i));
const folderItems = filter(item.items, (i) => isItemAFolder(i));
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
};
// we need to sort folder items by name alphabetically
const sortFolderItems = (items = []) => {
return items.sort((a, b) => a.name.localeCompare(b.name));
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
return (
<StyledWrapper className={className}>
@@ -114,7 +156,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
<div className={itemRowClassName}>
{runCollectionModalOpen && <RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />}
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full">
{indents && indents.length
? indents.map((i) => {
@@ -174,6 +217,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
>
New Folder
</div>
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
setRunCollectionModalOpen(true);
}}
>
Run
</div>
</>
)}
<div
@@ -212,13 +264,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
{!itemIsCollapsed ? (
<div>
{requestItems && requestItems.length
? requestItems.map((i) => {
{folderItems && folderItems.length
? folderItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
})
: null}
{folderItems && folderItems.length
? folderItems.map((i) => {
{requestItems && requestItems.length
? requestItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
})
: null}

View File

@@ -1,15 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
button.submit {
color: white;
background-color: var(--color-background-danger) !important;
border: inherit !important;
&:hover {
border: inherit !important;
}
}
`;
export default Wrapper;

View File

@@ -1,27 +0,0 @@
import React from 'react';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { deleteCollection } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DeleteCollection = ({ onClose, collection }) => {
const dispatch = useDispatch();
const onConfirm = () => {
dispatch(deleteCollection(collection.uid))
.then(() => {
toast.success('Collection deleted');
})
.catch(() => toast.error('An error occured while deleting the collection'));
};
return (
<StyledWrapper>
<Modal size="sm" title="Delete Collection" confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
Are you sure you want to delete the collection <span className="font-semibold">{collection.name}</span> ?
</Modal>
</StyledWrapper>
);
};
export default DeleteCollection;

View File

@@ -2,13 +2,13 @@ import React from 'react';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
const RemoveLocalCollection = ({ onClose, collection }) => {
const RemoveCollection = ({ onClose, collection }) => {
const dispatch = useDispatch();
const onConfirm = () => {
dispatch(removeLocalCollection(collection.uid))
dispatch(removeCollection(collection.uid))
.then(() => {
toast.success('Collection removed');
onClose();
@@ -23,4 +23,4 @@ const RemoveLocalCollection = ({ onClose, collection }) => {
);
};
export default RemoveLocalCollection;
export default RemoveCollection;

View File

@@ -1,35 +0,0 @@
import React from 'react';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { useSelector, useDispatch } from 'react-redux';
import { recursivelyGetAllItemUids } from 'utils/collections';
import { removeCollectionFromWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
const RemoveCollectionFromWorkspace = ({ onClose, collection }) => {
const dispatch = useDispatch();
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
const onConfirm = () => {
dispatch(removeCollectionFromWorkspace(activeWorkspaceUid, collection.uid))
.then(() => {
dispatch(
closeTabs({
tabUids: recursivelyGetAllItemUids(collection.items)
})
);
})
.then(() => dispatch(removeLocalCollection(collection.uid)))
.then(() => toast.success('Collection removed from workspace'))
.catch((err) => console.log(err) && toast.error('An error occured while removing the collection'));
};
return (
<Modal size="sm" title="Remove Collection from Workspace" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
Are you sure you want to remove the collection <span className="font-semibold">{collection.name}</span> from this workspace?
</Modal>
);
};
export default RemoveCollectionFromWorkspace;

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