mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
334 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19ca1af71e | ||
|
|
4016a83626 | ||
|
|
71b18c8b21 | ||
|
|
b53fcbb3d1 | ||
|
|
19a7f397bb | ||
|
|
a103f41d85 | ||
|
|
7d4d1573af | ||
|
|
6f2bb55ecf | ||
|
|
f189cb1a2e | ||
|
|
c020cd94a8 | ||
|
|
0d4f7e6a06 | ||
|
|
f9ed68843d | ||
|
|
d3a56fdc82 | ||
|
|
e6c3a5af4c | ||
|
|
cee8073bb7 | ||
|
|
69be52cf9e | ||
|
|
9e400085e3 | ||
|
|
b07bb67943 | ||
|
|
593210456a | ||
|
|
e328a4615e | ||
|
|
18afb73238 | ||
|
|
73b71e0829 | ||
|
|
4c25aa99aa | ||
|
|
23843b5d0a | ||
|
|
7fbd338fa6 | ||
|
|
4aeb5cf56d | ||
|
|
1ed39a5ea6 | ||
|
|
74f248782b | ||
|
|
99239e19b4 | ||
|
|
f46160e161 | ||
|
|
d0147778db | ||
|
|
87119cee2e | ||
|
|
b25d896dd6 | ||
|
|
08495e7fb5 | ||
|
|
ee084696f5 | ||
|
|
aeb9fc8875 | ||
|
|
26a05f92cb | ||
|
|
fff7819c46 | ||
|
|
1d678ee0d9 | ||
|
|
fd4c188c95 | ||
|
|
97678b05fc | ||
|
|
a1c9625aee | ||
|
|
a9d74467ff | ||
|
|
bd670eceb6 | ||
|
|
1ccb66e92a | ||
|
|
d62881fe0d | ||
|
|
9ea95b4571 | ||
|
|
df9322b767 | ||
|
|
c27c750c3e | ||
|
|
94baee8e25 | ||
|
|
417b50b0ad | ||
|
|
bf5ee7e409 | ||
|
|
3ca0107f1b | ||
|
|
ec22fdb637 | ||
|
|
66024d04e9 | ||
|
|
330a7ad18a | ||
|
|
2976842588 | ||
|
|
51e0ea2c2d | ||
|
|
3b85e7ebcc | ||
|
|
49b0f3a322 | ||
|
|
45ca5ded96 | ||
|
|
b6528062f0 | ||
|
|
86094cc054 | ||
|
|
8e0bc68ada | ||
|
|
c36c7b44a6 | ||
|
|
0ac27dee56 | ||
|
|
ede122ab09 | ||
|
|
96e368cb18 | ||
|
|
942b75861c | ||
|
|
c1711ea01b | ||
|
|
dedfefbc9a | ||
|
|
65dd5df87e | ||
|
|
78d2393686 | ||
|
|
0b65c4580e | ||
|
|
9cc1bf1e2f | ||
|
|
b96e3d0f23 | ||
|
|
11c99d55dc | ||
|
|
d054dc4c78 | ||
|
|
9014dc5769 | ||
|
|
d346970241 | ||
|
|
bdb3051c2b | ||
|
|
f8325b22b3 | ||
|
|
78251c530c | ||
|
|
dea95664b9 | ||
|
|
fbc6e7bff5 | ||
|
|
4884106aaa | ||
|
|
5c15438949 | ||
|
|
b53a9eaee9 | ||
|
|
5899ca446d | ||
|
|
d21e7f6fb5 | ||
|
|
ee8a3eae8c | ||
|
|
fac5109242 | ||
|
|
47dfbd2a64 | ||
|
|
074d72d885 | ||
|
|
8c29d131e2 | ||
|
|
437044bdcd | ||
|
|
2120a562da | ||
|
|
04c3c2dbf1 | ||
|
|
1d03e1d5ea | ||
|
|
2b174e1c60 | ||
|
|
7a2b32069e | ||
|
|
a9e6c3a35c | ||
|
|
e6a754b933 | ||
|
|
ee4509f037 | ||
|
|
c04f0e7a71 | ||
|
|
2f52ce4c71 | ||
|
|
b1edaba1c6 | ||
|
|
3f6fcdd582 | ||
|
|
c745786b1c | ||
|
|
9e30c7b440 | ||
|
|
b87cc7ccae | ||
|
|
1595d736f2 | ||
|
|
b38c25ca70 | ||
|
|
f22858219b | ||
|
|
8044286b80 | ||
|
|
34a2e23dc6 | ||
|
|
224b8c3cc4 | ||
|
|
d58e92205b | ||
|
|
925af1f26f | ||
|
|
d07744d5c2 | ||
|
|
5efb18ad63 | ||
|
|
9cfb54ee9f | ||
|
|
4c9d22d1e0 | ||
|
|
c5d43cc9e6 | ||
|
|
8300830a95 | ||
|
|
2dfc972930 | ||
|
|
4fdfdaf2cb | ||
|
|
a1385ba1e2 | ||
|
|
15804ac293 | ||
|
|
0244b2e1d6 | ||
|
|
7e70d05dc8 | ||
|
|
e60b06e4a4 | ||
|
|
17ded5de4c | ||
|
|
e1b97643bd | ||
|
|
8103554545 | ||
|
|
a425b42615 | ||
|
|
b14f867811 | ||
|
|
013abeaa80 | ||
|
|
9d3762702f | ||
|
|
cac9f9aef4 | ||
|
|
2b63368f2c | ||
|
|
acd980ffc6 | ||
|
|
1a175e4449 | ||
|
|
209f30998e | ||
|
|
e777eed00d | ||
|
|
15fc24679c | ||
|
|
48d26c05d9 | ||
|
|
9d395ded33 | ||
|
|
943e74c327 | ||
|
|
b852d1cc52 | ||
|
|
3d22f77226 | ||
|
|
429ca4093c | ||
|
|
a4f757ee87 | ||
|
|
df4f322024 | ||
|
|
ddd39e630d | ||
|
|
ef8e8bf637 | ||
|
|
7405fa9709 | ||
|
|
242fcac2d3 | ||
|
|
efd15838aa | ||
|
|
c55f9d42da | ||
|
|
2f32f7024e | ||
|
|
aff6499478 | ||
|
|
45ed47ff90 | ||
|
|
27c6c1349a | ||
|
|
c78ffa3a80 | ||
|
|
6b2d335ade | ||
|
|
837e39d870 | ||
|
|
67643c4c48 | ||
|
|
2b384656b6 | ||
|
|
411c06f4cb | ||
|
|
03fa46d8b3 | ||
|
|
d0f2eb27bc | ||
|
|
1b9ec05a58 | ||
|
|
3f74178c81 | ||
|
|
78ca6c5e96 | ||
|
|
5f59a16090 | ||
|
|
3805cef0c4 | ||
|
|
3c1a6ca71e | ||
|
|
d2227b2b05 | ||
|
|
dc03b6a761 | ||
|
|
e22f164cbc | ||
|
|
580d681e0a | ||
|
|
89b721d726 | ||
|
|
1110a4edda | ||
|
|
f69332d9c3 | ||
|
|
6947860204 | ||
|
|
963b0c257f | ||
|
|
33f8900705 | ||
|
|
22a14aa67a | ||
|
|
60c96f7d27 | ||
|
|
c8de57aa51 | ||
|
|
827c480689 | ||
|
|
1c869013c6 | ||
|
|
404a516fef | ||
|
|
e26075060e | ||
|
|
c524f40ab2 | ||
|
|
3e563ea126 | ||
|
|
a0cb53445f | ||
|
|
84bd603e11 | ||
|
|
c3236d4eb1 | ||
|
|
4a4208f272 | ||
|
|
d24f1a1054 | ||
|
|
86200a8f11 | ||
|
|
cf0ede1a83 | ||
|
|
342a39bcb4 | ||
|
|
e7d332c7d7 | ||
|
|
689d886e74 | ||
|
|
7a8e5198ff | ||
|
|
118ceacf46 | ||
|
|
a21615a5fb | ||
|
|
2ee2e270b0 | ||
|
|
9d6ba4691c | ||
|
|
104bd272f9 | ||
|
|
62a184c386 | ||
|
|
4663a1246c | ||
|
|
0efd782bcb | ||
|
|
a0903a5842 | ||
|
|
8202182074 | ||
|
|
ee4d4e3361 | ||
|
|
b76ddcd007 | ||
|
|
6f6dedbb9c | ||
|
|
37b1c043eb | ||
|
|
58bc247c53 | ||
|
|
c5b509115a | ||
|
|
524a59aed4 | ||
|
|
be49ef5f12 | ||
|
|
d4f05fa843 | ||
|
|
adedd08e8a | ||
|
|
7dd0d10a5d | ||
|
|
d9ef1692fe | ||
|
|
b88848f0dc | ||
|
|
abc26e5c5a | ||
|
|
d7733552bf | ||
|
|
6852cc6631 | ||
|
|
8bfb2591c2 | ||
|
|
05a290839b | ||
|
|
80f9e33be5 | ||
|
|
5a78dfa210 | ||
|
|
61caca59ee | ||
|
|
383c5ba782 | ||
|
|
28fbaa3470 | ||
|
|
27dcf78e73 | ||
|
|
25883b84fa | ||
|
|
667811cbd4 | ||
|
|
7839e93a57 | ||
|
|
11c60273b4 | ||
|
|
1dcff56c78 | ||
|
|
2e32423869 | ||
|
|
c328281f21 | ||
|
|
cc261326fc | ||
|
|
050ee2680f | ||
|
|
b2c28465e9 | ||
|
|
cd36335c60 | ||
|
|
d89f12c071 | ||
|
|
905f459ed0 | ||
|
|
b800055df4 | ||
|
|
b1d2b798ba | ||
|
|
4a403a253e | ||
|
|
a45628dd85 | ||
|
|
977637e556 | ||
|
|
3d63db806d | ||
|
|
1ec24d1138 | ||
|
|
fa40685a6a | ||
|
|
037013005f | ||
|
|
0c42298ce6 | ||
|
|
84ce75263b | ||
|
|
5c8d0a9e8a | ||
|
|
b70bbf78b1 | ||
|
|
43b9412ddb | ||
|
|
b56972fd93 | ||
|
|
c102ac527a | ||
|
|
45229b1af7 | ||
|
|
f9a3fb2f1b | ||
|
|
65d8a707d8 | ||
|
|
cc6bf45d5f | ||
|
|
8fbb777665 | ||
|
|
0e041d460c | ||
|
|
2e3b296021 | ||
|
|
405b50edcd | ||
|
|
fff540010e | ||
|
|
e513694912 | ||
|
|
d01cada16c | ||
|
|
dd4fecfd1c | ||
|
|
095d7c6bcb | ||
|
|
d165a04377 | ||
|
|
d3d1e47950 | ||
|
|
9c14941c15 | ||
|
|
1627f65bd7 | ||
|
|
19f4f3c1a5 | ||
|
|
ae70680ceb | ||
|
|
60fc13c765 | ||
|
|
60c3d41c8e | ||
|
|
fb8ff37d83 | ||
|
|
0d9b30e730 | ||
|
|
695f42df80 | ||
|
|
6b43159be2 | ||
|
|
21c9c8b4fb | ||
|
|
c4abe54c3f | ||
|
|
dd71c9e71b | ||
|
|
2be3e4bf69 | ||
|
|
f34e9f7b26 | ||
|
|
76b0729af3 | ||
|
|
4877bc3849 | ||
|
|
0742e3415c | ||
|
|
ae7e3a722c | ||
|
|
7f2e19250f | ||
|
|
4e16e954ef | ||
|
|
b6c3205474 | ||
|
|
23076b41c6 | ||
|
|
b5116b54af | ||
|
|
83aaa21b5b | ||
|
|
e1e7b37ce5 | ||
|
|
8dab9268f2 | ||
|
|
4eed999db1 | ||
|
|
c29ab50a3d | ||
|
|
5e1d6cba4a | ||
|
|
a645d1459c | ||
|
|
24e11a864c | ||
|
|
87a4778a91 | ||
|
|
0750af4c68 | ||
|
|
60e613fac8 | ||
|
|
b75baf57ba | ||
|
|
137df3c5c0 | ||
|
|
6ef2daebbd | ||
|
|
55f85e3728 | ||
|
|
f0269069d2 | ||
|
|
61dbca3243 | ||
|
|
f21cb240c4 | ||
|
|
ca46e14732 | ||
|
|
87f6000b85 | ||
|
|
36d0550472 | ||
|
|
ee4734c957 | ||
|
|
02f9fc0a7b | ||
|
|
6ce657d891 |
12
.github/workflows/bump-homebrew-cask.yml
vendored
Normal file
12
.github/workflows/bump-homebrew-cask.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Bump Homebrew Cask
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- name: Bump Homebrew Cask
|
||||
run: brew bump-cask-pr bruno --version "${GITHUB_REF_NAME#v}"
|
||||
29
.github/workflows/unit-tests.yml
vendored
Normal file
29
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
run: npm run build --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
BIN
assets/images/cli-demo.png
Normal file
BIN
assets/images/cli-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/images/landing-2.png
Normal file
BIN
assets/images/landing-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 436 KiB |
BIN
assets/images/run-anywhere.png
Normal file
BIN
assets/images/run-anywhere.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
assets/images/version-control.png
Normal file
BIN
assets/images/version-control.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -23,25 +23,7 @@ You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) an
|
||||
|
||||
### Lets start coding
|
||||
|
||||
```bash
|
||||
# clone and cd into bruno
|
||||
# use Node 14.x, Npm 8.x
|
||||
|
||||
# Install deps (note that we use npm workspaces)
|
||||
npm i
|
||||
|
||||
# run next app
|
||||
npm run dev:web
|
||||
|
||||
# run electron app
|
||||
# neededonly if you want to test changes related to electron app
|
||||
# please note that both web and electron use the same code
|
||||
# if it works in web, then it should also work in electron
|
||||
npm run dev:electron
|
||||
|
||||
# open in browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
|
||||
|
||||
### Raising Pull Request
|
||||
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
## development
|
||||
## Development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# run next app
|
||||
npm run dev --workspace=packages/bruno-app
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# run electron app
|
||||
npm run dev --workspace=packages/bruno-electron
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# build next app
|
||||
npm run build --workspace=packages/bruno-app
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
## fix
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
# testing
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
15
package.json
15
package.json
@@ -4,27 +4,36 @@
|
||||
"workspaces": [
|
||||
"packages/bruno-app",
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"randomstring": "^1.2.2"
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
"test:report": "npx playwright show-report",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 180
|
||||
"printWidth": 120
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT,
|
||||
ENV: process.env.ENV
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "cross-env ENV=dev next dev",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,8 +17,8 @@
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -26,7 +28,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
@@ -34,19 +36,20 @@
|
||||
"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",
|
||||
"reckonjs": "^0.1.2",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"yup": "^0.32.11"
|
||||
@@ -58,6 +61,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",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { get, post, put } from './base';
|
||||
|
||||
// not used. kept as a placeholder for reference while implementing license key stuff
|
||||
const AuthApi = {
|
||||
whoami: () => get('auth/v1/user/whoami'),
|
||||
signup: (params) => post('auth/v1/user/signup', params),
|
||||
login: (params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer
|
||||
.invoke('bruno-account-request', {
|
||||
data: params,
|
||||
method: 'POST',
|
||||
url: `${process.env.NEXT_PUBLIC_BRUNO_SERVER_API}/auth/v1/user/login`
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default AuthApi;
|
||||
@@ -1,30 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
headers: headers
|
||||
};
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
return Promise.reject(error.response ? error.response.data : error);
|
||||
}
|
||||
);
|
||||
|
||||
const { get, post, put, delete: destroy } = apiClient;
|
||||
|
||||
export { get, post, put, destroy };
|
||||
@@ -14,7 +14,11 @@ const Bruno = ({ width }) => {
|
||||
stroke="none"
|
||||
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
|
||||
/>
|
||||
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855" />
|
||||
<polygon
|
||||
fill="#3F3F3F"
|
||||
stroke="none"
|
||||
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
|
||||
/>
|
||||
</g>
|
||||
<g id="hair" />
|
||||
<g id="skin" />
|
||||
@@ -84,8 +88,27 @@ const Bruno = ({ width }) => {
|
||||
strokeWidth="2"
|
||||
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
|
||||
/>
|
||||
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632" />
|
||||
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
|
||||
/>
|
||||
<line
|
||||
x1="36.2078"
|
||||
x2="36.2078"
|
||||
y1="47.3393"
|
||||
y2="44.3093"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
@@ -8,6 +8,12 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
|
||||
@@ -6,13 +6,26 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
@@ -20,13 +33,20 @@ const StyledWrapper = styled.div`
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: green;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
@@ -15,7 +18,7 @@ if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class QueryEditor extends React.Component {
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,6 +26,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -39,6 +43,7 @@ export default class QueryEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-Enter': () => {
|
||||
@@ -70,6 +75,7 @@ export default class QueryEditor extends React.Component {
|
||||
}));
|
||||
if (editor) {
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +94,13 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
@@ -116,6 +128,15 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
const mode = this.props.mode || 'application/ld+json';
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
|
||||
@@ -5,7 +5,16 @@ import StyledWrapper from './StyledWrapper';
|
||||
const Dropdown = ({ icon, children, onCreate, placement }) => {
|
||||
return (
|
||||
<StyledWrapper className="dropdown">
|
||||
<Tippy content={children} placement={placement || 'bottom-end'} animation={false} arrow={false} onCreate={onCreate} interactive={true} trigger="click" appendTo="parent">
|
||||
<Tippy
|
||||
content={children}
|
||||
placement={placement || 'bottom-end'}
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
interactive={true}
|
||||
trigger="click"
|
||||
appendTo="parent"
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-enviroment {
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
|
||||
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -35,7 +35,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
toast.success(`No Environments are active now`);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('An error occured while selecting the environment'));
|
||||
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -63,13 +63,14 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<span>No Environment</span>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2">No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Settings</span>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,10 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
name: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addEnvironment(values.name, collection.uid))
|
||||
@@ -40,7 +43,13 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Create Environment'} confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Create Environment'}
|
||||
confirmText="Create"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
@@ -59,7 +68,9 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -20,7 +20,13 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Delete Environment'} confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Delete Environment'}
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
Are you sure you want to delete <span className="font-semibold">{environment.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -12,7 +12,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,12 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={variable.enabled} className="mr-3 mousetrap" onChange={(e) => handleVarChange(e, variable, 'enabled')} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVars(variable)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
@@ -119,7 +124,12 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mt-2" disabled={!hasChanges} onClick={saveChanges}>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary mt-2"
|
||||
disabled={!hasChanges}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,19 @@ import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
console.log(environment);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />}
|
||||
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />}
|
||||
{openEditModal && (
|
||||
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
{openDeleteModal && (
|
||||
<DeleteEnvironment
|
||||
onClose={() => setOpenDeleteModal(false)}
|
||||
environment={environment}
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -8,9 +10,36 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}, [environments]);
|
||||
if (selectedEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if (environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [collection, environments, selectedEnvironment]);
|
||||
|
||||
useEffect(() => {
|
||||
// check env add
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
|
||||
if (newEnv) {
|
||||
setSelectedEnvironment(newEnv);
|
||||
}
|
||||
}
|
||||
|
||||
// check env delete
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
@@ -25,7 +54,11 @@ const EnvironmentList = ({ collection }) => {
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div key={env.uid} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} onClick={() => setSelectedEnvironment(env)}>
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => setSelectedEnvironment(env)}
|
||||
>
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -16,7 +16,10 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
name: environment.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
||||
@@ -40,7 +43,13 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Rename Environment'} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'Rename Environment'}
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
@@ -59,7 +68,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -11,11 +11,21 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Environments" confirmText={'Close'} handleConfirm={onClose} handleCancel={onClose} hideCancel={true}>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Environments"
|
||||
confirmText={'Close'}
|
||||
handleConfirm={onClose}
|
||||
handleCancel={onClose}
|
||||
hideCancel={true}
|
||||
>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
<div className="text-center">
|
||||
<p>No environments found!</p>
|
||||
<button className="btn-create-environment text-link pr-2 py-3 mt-2 select-none" onClick={() => setOpenCreateModal(true)}>
|
||||
<button
|
||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={() => setOpenCreateModal(true)}
|
||||
>
|
||||
+ <span>Create Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
const SendIcon = ({color, width}) => {
|
||||
const SendIcon = ({ color, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z"/>
|
||||
<path d="M0 0h48v48h-48z" fill="none"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} viewBox="0 0 48 48">
|
||||
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z" />
|
||||
<path d="M0 0h48v48h-48z" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SendIcon;
|
||||
|
||||
@@ -100,7 +100,7 @@ const Wrapper = styled.div`
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out .1s;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
@@ -14,7 +14,15 @@ const ModalHeader = ({ title, handleCancel }) => (
|
||||
|
||||
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
|
||||
|
||||
const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel, hideFooter }) => {
|
||||
const ModalFooter = ({
|
||||
confirmText,
|
||||
cancelText,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter
|
||||
}) => {
|
||||
confirmText = confirmText || 'Save';
|
||||
cancelText = cancelText || 'Cancel';
|
||||
|
||||
@@ -30,7 +38,12 @@ const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, conf
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit}>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
disabled={confirmDisabled}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</span>
|
||||
@@ -38,7 +51,18 @@ const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, conf
|
||||
);
|
||||
};
|
||||
|
||||
const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfirm, children, confirmDisabled, hideCancel, hideFooter }) => {
|
||||
const Modal = ({
|
||||
size,
|
||||
title,
|
||||
confirmText,
|
||||
cancelText,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
children,
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter
|
||||
}) => {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const escFunction = (event) => {
|
||||
const escKeyCode = 27;
|
||||
@@ -64,7 +88,7 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
|
||||
if (isClosing) {
|
||||
classes += ' modal--animate-out';
|
||||
}
|
||||
if(hideFooter) {
|
||||
if (hideFooter) {
|
||||
classes += ' modal-footer-none';
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePreferences } from 'providers/Preferences';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const General = () => {
|
||||
const { preferences, setPreferences } = usePreferences();
|
||||
|
||||
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
request: {
|
||||
...preferences.request,
|
||||
sslVerification: !sslVerification
|
||||
}
|
||||
};
|
||||
|
||||
setPreferences(updatedPreferences)
|
||||
.then(() => {
|
||||
setSslVerification(!sslVerification);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="checkbox" checked={sslVerification} onChange={handleCheckboxChange} className="mr-3 mousetrap" />
|
||||
SSL Certificate Verification
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default General;
|
||||
@@ -0,0 +1,36 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
margin-top: -0.5rem;
|
||||
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.tab-panel {
|
||||
min-height: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: var(--color-text);
|
||||
.collection-options {
|
||||
color: ${(props) => props.theme.text};
|
||||
.rows {
|
||||
svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Support = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="rows">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Github</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandTwitter size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Twitter</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Support;
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: var(--color-text);
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
64
packages/bruno-app/src/components/Preferences/Theme/index.js
Normal file
64
packages/bruno-app/src/components/Preferences/Theme/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Theme = () => {
|
||||
const { storedTheme, setStoredTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
theme: storedTheme
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
setStoredTheme(values.theme);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-form">
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="light-theme"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit();
|
||||
}}
|
||||
value="light"
|
||||
checked={formik.values.theme === 'light'}
|
||||
/>
|
||||
<label htmlFor="light-theme" className="ml-1 cursor-pointer select-none">
|
||||
Light
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="dark-theme"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit();
|
||||
}}
|
||||
value="dark"
|
||||
checked={formik.values.theme === 'dark'}
|
||||
/>
|
||||
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Theme;
|
||||
54
packages/bruno-app/src/components/Preferences/index.js
Normal file
54
packages/bruno-app/src/components/Preferences/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import Support from './Support';
|
||||
import General from './General';
|
||||
import Theme from './Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Preferences = ({ onClose }) => {
|
||||
const [tab, setTab] = useState('general');
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === tab
|
||||
});
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'general': {
|
||||
return <General />;
|
||||
}
|
||||
|
||||
case 'theme': {
|
||||
return <Theme />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
||||
<div className="flex items-center px-2 tabs" role="tablist">
|
||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||
General
|
||||
</div>
|
||||
<div className={getTabClassname('theme')} role="tab" onClick={() => setTab('theme')}>
|
||||
Theme
|
||||
</div>
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow px-2 mt-4 tab-panel">{getTabPanel(tab)}</section>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preferences;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
* eq : equal to
|
||||
* neq : not equal to
|
||||
* gt : greater than
|
||||
* gte : greater than or equal to
|
||||
* lt : less than
|
||||
* lte : less than or equal to
|
||||
* in : in
|
||||
* notIn : not in
|
||||
* contains : contains
|
||||
* notContains : not contains
|
||||
* length : length
|
||||
* matches : matches
|
||||
* notMatches : not matches
|
||||
* startsWith : starts with
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
* isTruthy : is truthy
|
||||
* isFalsy : is falsy
|
||||
* isJson : is json
|
||||
* isNumber : is number
|
||||
* isString : is string
|
||||
* isBoolean : is boolean
|
||||
*/
|
||||
|
||||
const AssertionOperator = ({ operator, onChange }) => {
|
||||
const operators = [
|
||||
'eq',
|
||||
'neq',
|
||||
'gt',
|
||||
'gte',
|
||||
'lt',
|
||||
'lte',
|
||||
'in',
|
||||
'notIn',
|
||||
'contains',
|
||||
'notContains',
|
||||
'length',
|
||||
'matches',
|
||||
'notMatches',
|
||||
'startsWith',
|
||||
'endsWith',
|
||||
'between',
|
||||
'isEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
'isTruthy',
|
||||
'isFalsy',
|
||||
'isJson',
|
||||
'isNumber',
|
||||
'isString',
|
||||
'isBoolean'
|
||||
];
|
||||
|
||||
const handleChange = (e) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const getLabel = (operator) => {
|
||||
switch (operator) {
|
||||
case 'eq':
|
||||
return 'equals';
|
||||
case 'neq':
|
||||
return 'notEquals';
|
||||
default:
|
||||
return operator;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<select value={operator} onChange={handleChange} className="mousetrap">
|
||||
{operators.map((operator) => (
|
||||
<option key={operator} value={operator}>
|
||||
{getLabel(operator)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssertionOperator;
|
||||
@@ -0,0 +1,212 @@
|
||||
import React from 'react';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import AssertionOperator from '../AssertionOperator';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
* eq : equal to
|
||||
* neq : not equal to
|
||||
* gt : greater than
|
||||
* gte : greater than or equal to
|
||||
* lt : less than
|
||||
* lte : less than or equal to
|
||||
* in : in
|
||||
* notIn : not in
|
||||
* contains : contains
|
||||
* notContains : not contains
|
||||
* length : length
|
||||
* matches : matches
|
||||
* notMatches : not matches
|
||||
* startsWith : starts with
|
||||
* endsWith : ends with
|
||||
* between : between
|
||||
* isEmpty : is empty
|
||||
* isNull : is null
|
||||
* isUndefined : is undefined
|
||||
* isDefined : is defined
|
||||
* isTruthy : is truthy
|
||||
* isFalsy : is falsy
|
||||
* isJson : is json
|
||||
* isNumber : is number
|
||||
* isString : is string
|
||||
* isBoolean : is boolean
|
||||
*/
|
||||
const parseAssertionOperator = (str = '') => {
|
||||
if (!str || typeof str !== 'string' || !str.length) {
|
||||
return {
|
||||
operator: 'eq',
|
||||
value: str
|
||||
};
|
||||
}
|
||||
|
||||
const operators = [
|
||||
'eq',
|
||||
'neq',
|
||||
'gt',
|
||||
'gte',
|
||||
'lt',
|
||||
'lte',
|
||||
'in',
|
||||
'notIn',
|
||||
'contains',
|
||||
'notContains',
|
||||
'length',
|
||||
'matches',
|
||||
'notMatches',
|
||||
'startsWith',
|
||||
'endsWith',
|
||||
'between',
|
||||
'isEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
'isTruthy',
|
||||
'isFalsy',
|
||||
'isJson',
|
||||
'isNumber',
|
||||
'isString',
|
||||
'isBoolean'
|
||||
];
|
||||
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
'isTruthy',
|
||||
'isFalsy',
|
||||
'isJson',
|
||||
'isNumber',
|
||||
'isString',
|
||||
'isBoolean'
|
||||
];
|
||||
|
||||
const [operator, ...rest] = str.trim().split(' ');
|
||||
const value = rest.join(' ');
|
||||
|
||||
if (unaryOperators.includes(operator)) {
|
||||
return {
|
||||
operator,
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
|
||||
if (operators.includes(operator)) {
|
||||
return {
|
||||
operator,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
operator: 'eq',
|
||||
value: str
|
||||
};
|
||||
};
|
||||
|
||||
const isUnaryOperator = (operator) => {
|
||||
const unaryOperators = [
|
||||
'isEmpty',
|
||||
'isNull',
|
||||
'isUndefined',
|
||||
'isDefined',
|
||||
'isTruthy',
|
||||
'isFalsy',
|
||||
'isJson',
|
||||
'isNumber',
|
||||
'isString',
|
||||
'isBoolean'
|
||||
];
|
||||
|
||||
return unaryOperators.includes(operator);
|
||||
};
|
||||
|
||||
const AssertionRow = ({
|
||||
item,
|
||||
collection,
|
||||
assertion,
|
||||
handleAssertionChange,
|
||||
handleRemoveAssertion,
|
||||
onSave,
|
||||
handleRun
|
||||
}) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const { operator, value } = parseAssertionOperator(assertion.value);
|
||||
|
||||
return (
|
||||
<tr key={assertion.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={assertion.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<AssertionOperator
|
||||
operator={operator}
|
||||
onChange={(op) =>
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
value: `${op} ${value}`
|
||||
}
|
||||
},
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{!isUnaryOperator(operator) ? (
|
||||
<SingleLineEditor
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
readOnly={true}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
) : (
|
||||
<input type="text" className="cursor-default" disabled />
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assertion.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssertionRow;
|
||||
@@ -0,0 +1,60 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-assertion {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import AssertionRow from './AssertionRow';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Assertions = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
|
||||
|
||||
const handleAddAssertion = () => {
|
||||
dispatch(
|
||||
addAssertion({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleAssertionChange = (e, _assertion, type) => {
|
||||
const assertion = cloneDeep(_assertion);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
assertion.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
assertion.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
assertion.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateAssertion({
|
||||
assertion: assertion,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveAssertion = (assertion) => {
|
||||
dispatch(
|
||||
deleteAssertion({
|
||||
assertUid: assertion.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Expr</td>
|
||||
<td>Operator</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assertions && assertions.length
|
||||
? assertions.map((assertion) => {
|
||||
return (
|
||||
<AssertionRow
|
||||
key={assertion.uid}
|
||||
assertion={assertion}
|
||||
item={item}
|
||||
collection={collection}
|
||||
handleAssertionChange={handleAssertionChange}
|
||||
handleRemoveAssertion={handleRemoveAssertion}
|
||||
onSave={onSave}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
|
||||
+ Add Assertion
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Assertions;
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,19 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addFormUrlEncodedParam,
|
||||
updateFormUrlEncodedParam,
|
||||
deleteFormUrlEncodedParam
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +27,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +40,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +71,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,32 +92,33 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
|
||||
@@ -6,10 +6,16 @@ import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -18,26 +24,24 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||
const variables = item.draft
|
||||
? get(item, 'draft.request.body.graphql.variables')
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url);
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let { schema, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
if (!isSchemaLoading) {
|
||||
loadSchema();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(onSchemaLoad) {
|
||||
if (onSchemaLoad) {
|
||||
onSchemaLoad(schema);
|
||||
}
|
||||
}, [schema]);
|
||||
@@ -66,20 +70,38 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
value={query}
|
||||
onRun={onRun}
|
||||
onEdit={onQueryChange}
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>;
|
||||
return (
|
||||
<QueryEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
value={query}
|
||||
onRun={onRun}
|
||||
onEdit={onQueryChange}
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -107,23 +129,34 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
|
||||
Variables
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? (
|
||||
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
|
||||
) : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
|
||||
<div className="flex items-center cursor-pointer hover:underline" onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5} /> : null}
|
||||
<span className="ml-1">Schema</span>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center cursor-pointer hover:underline ml-2'
|
||||
onClick={toggleDocs}
|
||||
>
|
||||
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
|
||||
<div className="flex items-center cursor-pointer hover:underline ml-2" onClick={toggleDocs}>
|
||||
<IconBook size={18} strokeWidth={1.5} />
|
||||
<span className="ml-1">Docs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const fetchSchema = (endpoint) => {
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
});
|
||||
}
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [schema, setSchema] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if(!saved) {
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
return buildClientSchema(JSON.parse(saved));
|
||||
@@ -40,14 +25,14 @@ const useGraphqlSchema = (endpoint) => {
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchSchema(endpoint)
|
||||
.then((res) => res.json())
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('Graphql Schema loaded successfully');
|
||||
toast.success('GraphQL Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
@@ -55,7 +40,7 @@ const useGraphqlSchema = (endpoint) => {
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading Graphql Schema');
|
||||
toast.error('Error occured while loading GraphQL Schema');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={variables || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLVariables;
|
||||
@@ -7,6 +7,10 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
@@ -34,6 +38,18 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -59,7 +75,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Params
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
@@ -67,6 +83,18 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
@@ -75,7 +103,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,19 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +27,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +40,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +71,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,32 +92,33 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,20 @@ const StyledWrapper = styled.div`
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
@@ -22,13 +35,20 @@ const StyledWrapper = styled.div`
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: green;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import MD from 'markdown-it';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
@@ -29,6 +32,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -37,12 +41,17 @@ export default class QueryEditor extends React.Component {
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
// mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
},
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
scrollbarStyle: 'overlay',
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
foldGutter: {
|
||||
minFoldSize: 4
|
||||
@@ -129,6 +138,7 @@ export default class QueryEditor extends React.Component {
|
||||
editor.on('hasCompletion', this._onHasCompletion);
|
||||
editor.on('beforeChange', this._onBeforeChange);
|
||||
}
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -151,6 +161,11 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -163,6 +178,15 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: Overlay is messing up with schema hint
|
||||
// Fix this
|
||||
addOverlay = () => {
|
||||
// let variables = getAllVariables(this.props.collection);
|
||||
// this.variables = variables;
|
||||
// defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
// this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
|
||||
@@ -59,7 +59,10 @@ export default function onHasCompletion(_cm, data, onHintInformationRender) {
|
||||
const description = ctx.description ? md.render(ctx.description) : 'Self descriptive.';
|
||||
const type = ctx.type ? '<span className="infoType">' + renderType(ctx.type) + '</span>' : '';
|
||||
|
||||
information.innerHTML = '<div className="content">' + (description.slice(0, 3) === '<p>' ? '<p>' + type + description.slice(3) : type + description) + '</div>';
|
||||
information.innerHTML =
|
||||
'<div className="content">' +
|
||||
(description.slice(0, 3) === '<p>' ? '<p>' + type + description.slice(3) : type + description) +
|
||||
'</div>';
|
||||
|
||||
if (ctx && deprecation && ctx.deprecationReason) {
|
||||
const reason = ctx.deprecationReason ? md.render(ctx.deprecationReason) : '';
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -12,12 +13,20 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
|
||||
const handleAddParam = () => {
|
||||
@@ -20,6 +24,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
|
||||
@@ -32,10 +38,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -66,9 +68,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -90,32 +91,33 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
|
||||
@@ -10,7 +10,9 @@ const HttpMethodSelector = ({ method, onMethodSelect }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
|
||||
<div className="flex-grow font-medium" id="create-new-request-method">{method}</div>
|
||||
<div className="flex-grow font-medium" id="create-new-request-method">
|
||||
{method}
|
||||
</div>
|
||||
<div>
|
||||
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme } = useTheme();
|
||||
const { theme, storedTheme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector('.method-selector-container');
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@@ -38,19 +48,24 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
className="px-3 w-full mousetrap"
|
||||
type="text"
|
||||
<div
|
||||
className="flex items-center flex-grow input-container h-full"
|
||||
style={{
|
||||
color: 'yellow',
|
||||
width: `calc(100% - ${methodSelectorWidth}px)`,
|
||||
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
|
||||
}}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={(event) => onUrlChange(event.target.value)}
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -52,7 +52,7 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
onModeChange('formUrlEncoded');
|
||||
}}
|
||||
>
|
||||
Form Url Encoded
|
||||
Form URL Encoded
|
||||
</div>
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
|
||||
@@ -13,9 +13,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -45,7 +43,15 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
value={bodyContent[bodyMode] || ''}
|
||||
onEdit={onEdit}
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -19,6 +23,8 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
header.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -63,9 +65,8 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,32 +88,33 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={header.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={header.enabled} className="mr-3 mousetrap" onChange={(e) => handleHeaderValueChange(e, header, 'enabled')} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,14 @@ const SaveRequest = ({ items, onClose }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Save Request" confirmText="Save" cancelText="Cancel" handleCancel={onClose} handleConfirm={onClose}>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Save Request"
|
||||
confirmText="Save"
|
||||
cancelText="Cancel"
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onClose}
|
||||
>
|
||||
<p className="mb-2">Select a folder to save request:</p>
|
||||
<div className="folder-list">
|
||||
{showFolders && showFolders.length
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
||||
@@ -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;
|
||||
44
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
44
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestTests({
|
||||
tests: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const handleAddVar = () => {
|
||||
dispatch(
|
||||
addVar({
|
||||
type: varType,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
_var.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var, index) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
||||
54
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
54
packages/bruno-app/src/components/RequestPane/Vars/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Vars = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const RequestNotFound = ({ itemUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = () => {
|
||||
dispatch(
|
||||
@@ -13,11 +14,27 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
|
||||
if (!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||
<div>Request no longer exists.</div>
|
||||
<div className="mt-2">This can happen when the yml file associated with this request was deleted on your filesystem.</div>
|
||||
<div className="mt-2">
|
||||
This can happen when the .bru file associated with this request was deleted on your filesystem.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
Close Tab
|
||||
|
||||
@@ -12,6 +12,7 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -32,7 +33,9 @@ const RequestTabPanel = () => {
|
||||
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
|
||||
); // 2.2 so that request pane is relatively smaller
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
@@ -44,10 +47,10 @@ const RequestTabPanel = () => {
|
||||
const onSchemaLoad = (schema) => setSchema(schema);
|
||||
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
|
||||
const handleGqlClickReference = (reference) => {
|
||||
if(docExplorerRef.current) {
|
||||
if (docExplorerRef.current) {
|
||||
docExplorerRef.current.showDocForReference(reference);
|
||||
}
|
||||
if(!showGqlDocs) {
|
||||
if (!showGqlDocs) {
|
||||
setShowGqlDocs(true);
|
||||
}
|
||||
};
|
||||
@@ -65,10 +68,13 @@ const RequestTabPanel = () => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
let leftPaneXPosition = e.clientX + 2;
|
||||
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
|
||||
if (
|
||||
leftPaneXPosition < asideWidth + DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH ||
|
||||
leftPaneXPosition > screenWidth - MIN_RIGHT_PANE_WIDTH
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setLeftPaneWidth(leftPaneXPosition- asideWidth);
|
||||
setLeftPaneWidth(leftPaneXPosition - asideWidth);
|
||||
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
|
||||
}
|
||||
};
|
||||
@@ -112,6 +118,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} />;
|
||||
@@ -132,7 +143,13 @@ const RequestTabPanel = () => {
|
||||
</div>
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section className="request-pane">
|
||||
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
|
||||
<div
|
||||
className="px-4"
|
||||
style={{
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`,
|
||||
height: `calc(100% - ${DEFAULT_PADDING}px)`
|
||||
}}
|
||||
>
|
||||
{item.type === 'graphql-request' ? (
|
||||
<GraphQLRequestPane
|
||||
item={item}
|
||||
@@ -144,7 +161,9 @@ const RequestTabPanel = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} /> : null}
|
||||
{item.type === 'http-request' ? (
|
||||
<HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -159,19 +178,15 @@ const RequestTabPanel = () => {
|
||||
|
||||
{item.type === 'graphql-request' ? (
|
||||
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
|
||||
<DocExplorer schema={schema} ref={(r) => docExplorerRef.current = r}>
|
||||
<button
|
||||
className='mr-2'
|
||||
onClick={toggleDocs}
|
||||
aria-label="Close Documentation Explorer"
|
||||
>
|
||||
<DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>
|
||||
<button className="mr-2" onClick={toggleDocs} aria-label="Close Documentation Explorer">
|
||||
{'\u2715'}
|
||||
</button>
|
||||
</DocExplorer>
|
||||
</div>
|
||||
): null}
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabPanel;
|
||||
export default RequestTabPanel;
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
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 +25,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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -98,7 +87,15 @@ const RequestTab = ({ tab, collection }) => {
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="16" fill="#cc7b1b" className="has-changes-icon" viewBox="0 0 8 8">
|
||||
<svg
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="16"
|
||||
fill="#cc7b1b"
|
||||
className="has-changes-icon"
|
||||
viewBox="0 0 8 8"
|
||||
>
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -76,66 +76,83 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showRunner = activeCollection && activeCollection.showRunner;
|
||||
|
||||
// Todo: Must support ephermal requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
{newRequestModalOpen && <NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
{!showRunner ? (
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li
|
||||
key={tab.uid}
|
||||
className={getTabClassname(tab, index)}
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} 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" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import React from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = ({ value, width }) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRun = () => {
|
||||
if (disableRunEventListener) {
|
||||
return;
|
||||
}
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor theme={storedTheme} value={value || ''} readOnly />
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
onRun={onRun}
|
||||
value={value || ''}
|
||||
mode={mode}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,11 @@ import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
&.text-ok {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseOk};
|
||||
}
|
||||
|
||||
|
||||
&.text-error {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseError};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TestResults = ({ results, assertionResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
return <div className="px-3">No tests found</div>;
|
||||
}
|
||||
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass');
|
||||
const failedAssertions = assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col px-3">
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">✔ {result.description}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">✘ {result.description}</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '}
|
||||
{failedAssertions.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{assertionResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResults;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
const numberOfTests = results.length;
|
||||
const numberOfFailedTests = results.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const numberOfAssertions = assertionResults.length;
|
||||
const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div>Tests</div>
|
||||
{totalNumberOfFailedTests ? (
|
||||
<sup className="sups some-tests-failed ml-1 font-medium">{totalNumberOfFailedTests}</sup>
|
||||
) : (
|
||||
<sup className="sups all-tests-passed ml-1 font-medium">{totalNumberOfTests}</sup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResultsLabel;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
import Overlay from './Overlay';
|
||||
@@ -10,13 +11,16 @@ import ResponseHeaders from './ResponseHeaders';
|
||||
import StatusCode from './StatusCode';
|
||||
import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = item.response && item.response.state === 'sending';
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -32,11 +36,27 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
|
||||
return (
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={
|
||||
response.data ? (isJson(response.headers) ? safeStringifyJSON(response.data, true) : response.data) : ''
|
||||
}
|
||||
mode={getContentType(response.headers)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={item.requestSent} response={item.response} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
@@ -66,7 +86,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
|
||||
return <div className="pb-4 px-4">An error occured!</div>;
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
@@ -75,6 +95,28 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getContentType = (headers) => {
|
||||
if (headers && headers.length) {
|
||||
let contentType = headers
|
||||
.filter((header) => header[0].toLowerCase() === 'content-type')
|
||||
.map((header) => {
|
||||
return header[1];
|
||||
});
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const isJson = (headers) => {
|
||||
return getContentType(headers) === 'application/ld+json';
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
@@ -84,6 +126,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={response.status} />
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const { requestSent, responseReceived, testResults } = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', {});
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
|
||||
const selectTab = (tab) => setSelectedTab(tab);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return (
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
disableRunEventListener={true}
|
||||
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
<ResponseTime duration={duration} />
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsePane;
|
||||
@@ -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;
|
||||
232
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
232
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import slash from 'utils/common/slash';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
// convert to unix style path
|
||||
fullPath = slash(fullPath);
|
||||
pathname = slash(pathname);
|
||||
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir, name } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
item.name = info.name;
|
||||
item.type = info.type;
|
||||
item.filename = info.filename;
|
||||
item.pathname = info.pathname;
|
||||
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||
|
||||
if (item.status !== 'error') {
|
||||
if (item.testResults) {
|
||||
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.testStatus = 'pass';
|
||||
}
|
||||
|
||||
if (item.assertionResults) {
|
||||
const failed = item.assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.assertionStatus = 'pass';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
};
|
||||
|
||||
const closeRunner = () => {
|
||||
dispatch(
|
||||
closeCollectionRunner({
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter((item) => {
|
||||
return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
});
|
||||
const failedRequests = items.filter((item) => {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="px-4">
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4">
|
||||
<div className="font-medium mt-6 mb-4 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== 'error' && item.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
>
|
||||
{item.relativePath}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(<span className="mr-1">{get(item.responseReceived, 'status')}</span>
|
||||
<span>{get(item.responseReceived, 'statusText')}</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults
|
||||
? item.assertionResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-1" style={{ width: '50%' }}>
|
||||
{selectedItem ? (
|
||||
<div className="flex flex-col w-full overflow-auto">
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.relativePath}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -16,11 +17,19 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
name: item.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occured while cloning the request');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -33,7 +42,13 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title={`Clone ${isFolder ? 'Folder' : 'Request'}`} confirmText="Clone" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Clone"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
|
||||
@@ -31,7 +31,13 @@ const DeleteCollectionItem = ({ onClose, item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={`Delete ${isFolder ? 'Folder' : 'Request'}`} confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={`Delete ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
Are you sure you want to delete <span className="font-semibold">{item.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -16,7 +16,10 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
name: item.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid));
|
||||
@@ -33,7 +36,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title={`Rename ${isFolder ? 'Folder' : 'Request'}`} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user