Compare commits

...

100 Commits

Author SHA1 Message Date
Anusree Subash
41d9bbd2d8 fix: fixed issue renaming workspaces and creating collections 2022-10-22 16:03:26 +05:30
Anoop M D
ac4e3a9f3d chore: rollback temporary fix to make the vercel build pass 2022-10-22 00:47:24 +05:30
Anoop M D
0ecaba27a6 fix: temporary fix for failing vercel build since its not finding nextjs in root 2022-10-22 00:37:55 +05:30
Anoop M D
2c8ef7b626 chore: bumped nextjs to 12.3.1 2022-10-22 00:34:01 +05:30
Bram Hoven
ea3a9394c9 Improve error for workspace deletion (#39)
* Move dmg-license to optionalDependencies so it can be installed on windows
* Extend toastError to accept a default error message
* Throw BrunoError when in deleteWorkspace
* Handle errors with the toastError
* Use existing parseError for getting errorMsg in toastError
2022-10-21 23:57:42 +05:30
Anoop M D
995c6b3fd0 chore: updated landing image 2022-10-21 18:46:23 +05:30
Anoop M D
bd6ce6a67b feat: added contributing guide 2022-10-21 18:41:28 +05:30
Sean
3c3c9a6026 Added use local storage hook (#36) 2022-10-20 16:30:35 +05:30
Anoop M D
6b68857b81 chore: added package-lock.json to tauri and testbench packages 2022-10-20 15:18:09 +05:30
Anoop M D
405f253eef chore: deleted package-lock.json in tauri and testbench packages 2022-10-20 15:17:16 +05:30
Anoop M D
4b6439785f chore: added package-lock.json to gitignore 2022-10-20 15:16:33 +05:30
Anoop M D
f9806e69a5 Merge branch 'main' of github.com:usebruno/bruno 2022-10-20 15:11:15 +05:30
Anoop M D
ba219d66db feat: prettier config 2022-10-20 15:09:30 +05:30
Sean
a41f4fe024 fix: dependency error when contributing (#34) 2022-10-19 19:34:42 +05:30
Anoop M D
93544f8ae6 chore: fix typo 2022-10-18 05:33:57 +05:30
Anoop M D
d6e4d07e2c chore: collections page disabled until fix is available in electron 2022-10-18 05:12:56 +05:30
Anoop M D
503f0b8a17 feat: electron windows build 2022-10-18 05:05:49 +05:30
Anoop M D
5fc9bbd729 fix: fixed issue while saving json in local collections 2022-10-18 04:26:28 +05:30
Anoop M D
eefef27dec feat: error messaging when attempting to create duplicate files or folders in local collections 2022-10-18 03:44:21 +05:30
Anoop M D
e98f219448 feat: local collections environment sync 2022-10-18 03:39:36 +05:30
Anoop M D
ff87586a1d feat: bruno.json validation for local collections 2022-10-18 02:34:22 +05:30
Anoop M D
8a96a0ce71 feat: wireup local collections open and create buttons 2022-10-18 01:27:53 +05:30
Anoop M D
9fae7f72d4 feat: electron build for linux 2022-10-17 22:17:03 +05:30
Anoop M D
ad1824e473 fix: star button no loading in electron 2022-10-17 22:02:55 +05:30
Anoop M D
8045751671 chore: svgs are moved inside src folder 2022-10-17 21:40:57 +05:30
Anoop M D
c258bc1590 feat: electron build for mac 2022-10-17 21:04:05 +05:30
Anoop M D
075e9162c2 feat: chrome extension migrate to manifest v3 2022-10-17 18:18:27 +05:30
Anoop M D
d91ee36192 feat: chrome extension 2022-10-17 03:57:25 +05:30
Anoop M D
579bd424fc chore: added bruno-cli package 2022-10-17 03:07:15 +05:30
Anoop M D
78645ad52f feat: published @usebruno/schema 2022-10-17 03:03:32 +05:30
Anoop M D
241ee5e788 feat: sample collection + bug fixes 2022-10-17 00:39:58 +05:30
Anoop M D
6573df41b0 feat: hotkeys (ctrl.cmd E, B, H) 2022-10-16 23:58:04 +05:30
Anoop M D
fe900b90c9 feat: version number in collection schema 2022-10-16 23:36:10 +05:30
Anoop M D
7078d5cec2 feat: beteer cors error messaging 2022-10-16 22:42:29 +05:30
Anoop M D
46949e48ba feat: wip packaging chrome extension 2022-10-16 20:01:23 +05:30
Anoop M D
abc00b810f feat: interpolate environment vats while sending request 2022-10-16 19:25:47 +05:30
Anoop M D
510e549d34 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-16 18:51:07 +05:30
Anoop M D
42a60a3372 feat: persist selected environment inside collection 2022-10-16 18:51:02 +05:30
anusreesubash
f1aaf862ae Feature/support links (#30)
* feat: added support links
* chore: fixed lint issues
2022-10-16 18:29:00 +05:30
Anoop M D
ecc2252e84 feat: persist active workspace in local storage 2022-10-16 17:24:30 +05:30
Anoop M D
2efc11ff6b feat: environment variables grid 2022-10-16 16:40:54 +05:30
Anoop M D
6a36313e0e fix: fixed error during item rename 2022-10-16 14:51:47 +05:30
Anoop M D
6380797f92 fix: fixed bug where collection was not getting created 2022-10-16 14:40:43 +05:30
Anoop M D
d8f58aeb0d chore: local collections unavailable msg on web 2022-10-16 14:34:09 +05:30
Anoop M D
0f5b75ddbf chore: updated readme 2022-10-16 13:51:10 +05:30
Anoop M D
7ca6270f2b feat: connect environments to redux store 2022-10-16 05:46:49 +05:30
Anoop M D
c6ac90a9f8 Merge branch 'feature/environment-configuration' into main 2022-10-16 04:05:59 +05:30
Anoop M D
d640dafb06 chore: collection uid check before pushing collections into state 2022-10-16 04:03:00 +05:30
Anoop M D
d8cdd2ad8b feat: request cancel implementation in electron 2022-10-16 03:06:46 +05:30
Anoop M D
118658822d feat: strike of collection from electron-store upon removal 2022-10-16 02:21:48 +05:30
Anoop M D
0709666b03 feat: woekspace switch modal 2022-10-16 02:18:56 +05:30
Anoop M D
fad953a983 feat: auto hydrate last opened collections 2022-10-16 02:06:58 +05:30
Anusree Subash
4a5378a2e1 feat: environment configuration (resolves #8) 2022-10-16 01:15:36 +05:30
Anoop M D
75f6daec06 chore: cleanup 2022-10-16 01:07:45 +05:30
Anoop M D
f2ffca35da feat: local collections displayed separately (resolves #22) 2022-10-16 01:05:52 +05:30
Anoop M D
c95bc8fdf9 feat: remove local collection from workspace (resolves #22) 2022-10-15 21:22:25 +05:30
Anoop M D
44aa019754 feat: local filesystem collections (resolves #22) 2022-10-15 20:14:43 +05:30
Anoop M D
91981a48e4 feat: collections are stored as objects in workspaces 2022-10-15 13:50:58 +05:30
Anoop M D
d546709b26 feat: import collections 2022-10-15 04:04:45 +05:30
Anoop M D
6feca9937e feat: export collections 2022-10-15 03:07:50 +05:30
Anoop M D
4ff268712f feat: added schema validation before saving collections to idb 2022-10-15 02:48:06 +05:30
Anoop M D
a78bdf87fe redactor: moved all reducer collection actions needing idb access to actions file 2022-10-15 02:08:35 +05:30
Anoop M D
a84080b482 fix: activeTabUid was not being reset to null 2022-10-15 01:45:15 +05:30
Anoop M D
b3bf29d6b2 feat: collection schema definition 2022-10-15 01:15:56 +05:30
Anoop M D
013f9f9e3d feat: url bar now occupies full width of the panel 2022-10-15 00:35:00 +05:30
Anoop M D
e46e3d5b22 feat: yup schema should not allow unknown keys 2022-10-14 22:42:46 +05:30
Anoop M D
8763ff2ad1 feat: glue workspace schema validations 2022-10-14 22:35:02 +05:30
Anoop M D
7ddfac1ece feat: bruno schema definition for workspace 2022-10-14 21:51:59 +05:30
Anoop M D
6bb3967379 feat: improved tab behaviour while closing a tab 2022-10-14 02:12:59 +05:30
Anoop M D
d49eb4df33 chore: cleanup 2022-10-14 01:59:24 +05:30
Anoop M D
410bc70318 feat: cancel running request (resolves #26) 2022-10-14 01:34:15 +05:30
Anoop M D
097a6240ad chore: refactor request type names 2022-10-14 00:43:03 +05:30
Anoop M D
6b0ccac1bf feat: support adding and removing collections from workspaces 2022-10-14 00:20:02 +05:30
Anoop M D
f8fbc88239 chore: moved next deps to bruno-app package 2022-10-13 22:08:36 +05:30
Anoop M D
819e8c2ccd feat: sync workspaces with idb 2022-10-13 05:23:54 +05:30
Anoop M D
008704c4e1 feat: load workspaces from idb 2022-10-12 04:41:26 +05:30
Anoop M D
1b2097250e chrore: added icon for reporting issues 2022-10-11 03:56:27 +05:30
Anoop M D
42984ce931 chore: updated bruno-schema readme 2022-10-11 03:50:48 +05:30
Anoop M D
aed737ed33 chore: cleanup 2022-10-11 03:42:48 +05:30
Anoop M D
0bd51b8a01 feat: bruno schema init 2022-10-11 03:39:26 +05:30
Anoop M D
02ff85cc57 feat: delete collections 2022-10-11 03:20:50 +05:30
Anoop M D
adc6be031d feat: toast integration 2022-10-11 02:29:19 +05:30
Anoop M D
6476b47d53 feat: rename collection 2022-10-11 02:11:52 +05:30
Anoop M D
62e9f4d5f0 chore: added null safety while searching 2022-10-11 01:35:46 +05:30
Anoop M D
77568da03c feat: show home page upon clicking title bar 2022-10-11 01:28:31 +05:30
Anoop M D
be72fbfe6f chore: cleanup 2022-10-10 23:27:27 +05:30
Anoop M D
b9ab5e572d chore: updated request tab method name font size 2022-10-10 05:34:04 +05:30
Anoop M D
09c6feed98 chore: updated readme 2022-10-10 04:26:47 +05:30
Anoop M D
a54d6fe6d7 feat: layout redesign 2022-10-10 04:10:45 +05:30
Anoop M D
9c5d66e7db chore: added comparision table in readme 2022-10-10 01:37:39 +05:30
Anoop M D
bacc8a1084 chore: added links in home page 2022-10-10 00:01:04 +05:30
Anoop M D
50ae592e1e feat: collections page (resolves #24) 2022-10-09 23:15:46 +05:30
Anoop M D
c8957f5555 chore: placeholder for local collections in welcome page (#22) 2022-10-09 22:20:52 +05:30
Anoop M D
fba3f24568 chore: updated icon used for collection 2022-10-09 16:32:44 +05:30
Anoop M D
539cdef9ca feat: moved next app to its own package (resolves #21) 2022-10-09 16:13:11 +05:30
anusreesubash
b3a317dc4d feat: workspaces crud (resolves #15) (#19)
feat: workspaces crud (resolves #15)
2022-10-09 12:45:48 +05:30
Anoop M D
f634839adb feat: tauri builder package (resolves #18) 2022-10-09 12:34:09 +05:30
Anoop M D
2ac1b3639d feat: moved bruno testbench into packages/ (feat: resolves #17) 2022-10-08 20:00:36 +05:30
Anoop M D
64bffc8216 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-08 19:53:49 +05:30
Anoop M D
1dd808ed20 feat: package for electron-builder (resolves #16) 2022-10-08 19:53:30 +05:30
317 changed files with 13447 additions and 22272 deletions

8
.gitignore vendored
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

44
contributing.md Normal file
View File

@@ -0,0 +1,44 @@
## Lets make bruno better, together !!
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computed.
### Technology Stack
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use
* CSS - Tailwind
* Code Editors - Codemirror
* State Management - Redux
* Icons - Tabler Icons
* Forms - formik
* Schema Validation - Yup
* Request Client - axios
* Filesystem Watcher - chokidar
### Dependencies
You would need Node v14.x and npm 8.x. We use npm workspaces in the project
### Lets start coding
```bash
# clone and cd into bruno
# use Node 14.x, Npm 8.x
# 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
```
### Raising Pull Request
* Please keep the PR's small and focused on one thing

View File

@@ -1 +1,27 @@
## development
```bash
# install deps
npm i
# run next app
npm run dev --workspace=packages/bruno-app
# run electron app
npm run dev --workspace=packages/bruno-electron
# build next app
npm run build --workspace=packages/bruno-app
```
## fix
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
# testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
```

18494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
{
"name": "@usebruno/app",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/schema": "0.1.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"codemirror-graphql": "^1.2.5",
"escape-html": "^1.0.3",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"graphiql": "^1.5.9",
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.1",
"path": "^0.12.7",
"qs": "^6.11.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.4.0",
"react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-spread": "^7.16.7",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"babel-loader": "^8.2.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.7.1",
"style-loader": "^3.3.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

View File

@@ -6,17 +6,18 @@ const AuthApi = {
signup: (params) => post('auth/v1/user/signup', params),
login: (params) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window.require("electron");
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('grafnode-account-request', {
data: params,
method: 'POST',
url: `${process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API}/auth/v1/user/login`,
})
.then(resolve)
.catch(reject);
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;
export default AuthApi;

View File

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

View File

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

After

Width:  |  Height:  |  Size: 814 B

View File

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

After

Width:  |  Height:  |  Size: 211 B

View File

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

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: var(--color-text);
.collection-options {
svg {
position: relative;
top: -1px;
}
.label {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import Modal from 'components/Modal/index';
import { IconSpeakerphone, IconBrandTwitter } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import GithubSvg from 'assets/github.svg';
const BrunoSupport = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
<div className="collection-options">
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<img src={GithubSvg.src} style={{ width: '18px' }} />
<span className="label ml-2">Github</span>
</a>
</div>
<div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-center">
<IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span>
</a>
</div>
</div>
</Modal>
</StyledWrapper>
);
};
export default BrunoSupport;

View File

@@ -11,4 +11,3 @@ const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@@ -37,33 +37,33 @@ export default class QueryEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly ? 'nocursor' : false,
extraKeys: {
'Cmd-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Tab': function(cm){
cm.replaceSelection(" " , "end");
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
}
},
}
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -82,14 +82,10 @@ export default class QueryEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
this.editor.setOption("mode", this.props.mode);
this.editor.setOption('mode', this.props.mode);
}
this.ignoreChangeEvent = false;
}
@@ -106,7 +102,7 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full"
aria-label="Code Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>

View File

@@ -24,13 +24,13 @@ const Wrapper = styled.div`
.label-item {
display: flex;
align-items: center;
padding: .35rem .6rem;
padding: 0.35rem 0.6rem;
}
.dropdown-item {
display: flex;
align-items: center;
padding: .35rem .6rem;
padding: 0.35rem 0.6rem;
cursor: pointer;
&:hover {

View File

@@ -0,0 +1,15 @@
import React from 'react';
import Tippy from '@tippyjs/react';
import StyledWrapper from './StyledWrapper';
const Dropdown = ({ icon, children, onCreate, placement }) => {
return (
<StyledWrapper className="dropdown">
<Tippy content={children} placement={placement || 'bottom-end'} animation={false} arrow={false} onCreate={onCreate} interactive={true} trigger="click" appendTo="parent">
{icon}
</Tippy>
</StyledWrapper>
);
};
export default Dropdown;

View File

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

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: ''
},
validationSchema: Yup.object({
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
}),
onSubmit: (values) => {
dispatch(addEnvironment(values.name, collection.uid))
.then(() => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occured while created the environment'));
}
});
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
};
return (
<Portal>
<Modal size="sm" title={'Create Environment'} confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
</Portal>
);
};
export default CreateEnvironment;

View File

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

View File

@@ -6,7 +6,8 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
thead,
td {
border: 1px solid #efefef;
}
@@ -24,18 +25,18 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
.environments-sidebar {
background-color: #eaeaea;
min-height: 400px;
}
.environment-item {
min-width: 150px;
display: block;
position: relative;
cursor: pointer;
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: #e4e4e4;
}
}
.active {
background-color: #dcdcdc !important;
border-left: solid 2px var(--color-brand);
&:hover {
background-color: #dcdcdc !important;
}
}
.btn-create-environment {
padding: 8px 10px;
cursor: pointer;
border-bottom: none;
color: var(--color-text-link);
&:hover {
span {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,43 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment/index';
import StyledWrapper from './StyledWrapper';
const EnvironmentList = ({ collection }) => {
const { environments } = collection;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
useEffect(() => {
setSelectedEnvironment(environments[0]);
}, []);
if (!selectedEnvironment) {
return null;
}
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
<div className="flex">
<div>
<div className="environments-sidebar">
{environments &&
environments.length &&
environments.map((env) => (
<div key={env.uid} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} onClick={() => setSelectedEnvironment(env)}>
<span>{env.name}</span>
</div>
))}
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
+ <span>Create</span>
</div>
</div>
</div>
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
</div>
</StyledWrapper>
);
};
export default EnvironmentList;

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: environment.name
},
validationSchema: Yup.object({
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
}),
onSubmit: (values) => {
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
.then(() => {
toast.success('Environment renamed successfully');
onClose();
})
.catch(() => toast.error('An error occured while renaming the environment'));
}
});
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
};
return (
<Portal>
<Modal size="sm" title={'Rename Environment'} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
</Portal>
);
};
export default RenameEnvironment;

View File

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

View File

@@ -0,0 +1,34 @@
import Modal from 'components/Modal/index';
import React, { useState } from 'react';
import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
const EnvironmentSettings = ({ collection, onClose }) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
if (!environments || !environments.length) {
return (
<StyledWrapper>
<Modal size="md" title="Environments" confirmText={'Close'} handleConfirm={onClose} handleCancel={onClose} hideCancel={true}>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
<div className="text-center">
<p>No environments found!</p>
<button className="btn-create-environment text-link pr-2 py-3 mt-2 select-none" onClick={() => setOpenCreateModal(true)}>
+ <span>Create Environment</span>
</button>
</div>
</Modal>
</StyledWrapper>
);
}
return (
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList collection={collection} />
</Modal>
);
};
export default EnvironmentSettings;

View File

@@ -1,11 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div`
&.modal--animate-out{
animation: fade-out 0.5s forwards cubic-bezier(.19,1,.22,1);
&.modal--animate-out {
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
.bruno-modal-card {
animation: fade-and-slide-out-from-top .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-out-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
}
@@ -23,8 +23,8 @@ const Wrapper = styled.div`
}
.bruno-modal-card {
animation-duration: .85s;
animation-delay: .1s;
animation-duration: 0.85s;
animation-delay: 0.1s;
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
@@ -33,28 +33,32 @@ const Wrapper = styled.div`
box-shadow: var(--box-shadow-base);
display: flex;
flex-direction: column;
will-change: opacity,transform;
will-change: opacity, transform;
flex-grow: 0;
margin: 3vh 10vw;
margin-top: 50px;
&.modal-sm {
min-width: 300px;
max-width: 500px;
}
&.modal-md {
min-width: 500px;
max-width: 800px;
}
&.modal-lg {
min-width: 800px;
max-width: 1140px;
}
&.modal-xl {
min-width: 1140px;
max-width: calc(100% - 30px);
}
animation: fade-and-slide-in-from-top .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-header {
@@ -98,18 +102,18 @@ const Wrapper = styled.div`
will-change: opacity;
background: transparent;
&:before{
content: "";
&:before {
content: '';
height: 100%;
width: 100%;
left: 0;
opacity: .4;
opacity: 0.4;
top: 0;
background: black;
position: fixed;
}
animation: fade-in .1s forwards cubic-bezier(.19,1,.22,1);
animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-footer {

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
thead,
td {
border: 1px solid #efefef;
}
@@ -24,18 +25,18 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,133 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
const addParam = () => {
dispatch(
addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateFormUrlEncodedParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteFormUrlEncodedParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
</StyledWrapper>
);
};
export default FormUrlEncodedParams;

View File

@@ -18,7 +18,11 @@ const StyledWrapper = styled.div`
color: rgb(125 125 125);
outline: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
@@ -38,7 +42,11 @@ const StyledWrapper = styled.div`
outline: none !important;
box-shadow: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
border: none;
outline: none !important;
box-shadow: none !important;
@@ -49,7 +57,6 @@ const StyledWrapper = styled.div`
box-shadow: none !important;
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -4,23 +4,17 @@ import QueryEditor from 'components/RequestPane/QueryEditor';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import StyledWrapper from './StyledWrapper';
const GraphQLRequestPane = ({onRunQuery, schema, leftPaneWidth, value, onQueryChange}) => {
const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
return (
<StyledWrapper className="h-full">
<Tabs className='react-tabs mt-1 flex flex-grow flex-col h-full' forceRenderTabPanel>
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
<TabList>
<Tab tabIndex="-1">Query</Tab>
<Tab tabIndex="-1">Headers</Tab>
</TabList>
<TabPanel>
<div className="mt-4">
<QueryEditor
schema={schema}
width={leftPaneWidth}
value={value}
onRunQuery={onRunQuery}
onEdit={onQueryChange}
/>
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
</div>
</TabPanel>
<TabPanel>
@@ -28,7 +22,7 @@ const GraphQLRequestPane = ({onRunQuery, schema, leftPaneWidth, value, onQueryCh
</TabPanel>
</Tabs>
</StyledWrapper>
)
);
};
export default GraphQLRequestPane;

View File

@@ -10,7 +10,11 @@ const StyledWrapper = styled.div`
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
@@ -23,4 +27,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -9,73 +9,75 @@ import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import StyledWrapper from './StyledWrapper';
const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const selectTab = (tab) => {
dispatch(updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
}))
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const getTabPanel = (tab) => {
switch(tab) {
switch (tab) {
case 'params': {
return <QueryParams item={item} collection={collection}/>;
return <QueryParams item={item} collection={collection} />;
}
case 'body': {
return <RequestBody item={item} collection={collection}/>;
return <RequestBody item={item} collection={collection} />;
}
case 'headers': {
return <RequestHeaders item={item} collection={collection}/>;
return <RequestHeaders item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
}
}
};
if(!activeTabUid) {
return (
<div>Something went wrong</div>
);
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if(!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === focusedTab.requestPaneTab
active: tabName === focusedTab.requestPaneTab
});
};
return (
<StyledWrapper className="flex flex-col h-full relativ">
<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</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>Body</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>Headers</div>
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Params
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
{/* Moved to post mvp */}
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
{focusedTab.requestPaneTab === 'body' ? (
<div className="flex flex-grow justify-end items-center">
<RequestBodyMode item={item} collection={collection}/>
<RequestBodyMode item={item} collection={collection} />
</div>
) : null }
) : null}
</div>
<section className="flex w-full mt-5">
{getTabPanel(focusedTab.requestPaneTab)}
</section>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
)
);
};
export default HttpRequestPane;

View File

@@ -0,0 +1,46 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
thead,
td {
border: 1px solid #efefef;
}
thead {
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
}
}
.btn-add-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,133 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
const addParam = () => {
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateMultipartFormParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteMultipartFormParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
</StyledWrapper>
);
};
export default MultipartFormParams;

View File

@@ -13,4 +13,3 @@ const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@@ -44,44 +44,37 @@ export default class QueryEditor extends React.Component {
showCursorWhenSelecting: true,
readOnly: this.props.readOnly ? 'nocursor' : false,
foldGutter: {
minFoldSize: 4,
minFoldSize: 4
},
lint: {
schema: this.props.schema,
validationRules: this.props.validationRules ?? null,
// linting accepts string or FragmentDefinitionNode[]
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
hintOptions: {
schema: this.props.schema,
closeOnUnfocus: false,
completeSingle: false,
container: this._node,
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
info: {
schema: this.props.schema,
renderDescription: (text) => md.render(text),
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference),
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
jump: {
schema: this.props.schema,
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference)
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: {
'Cmd-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Enter': () => {
if (this.props.onRunQuery) {
@@ -129,8 +122,8 @@ export default class QueryEditor extends React.Component {
if (this.props.onRunQuery) {
// empty
}
},
},
}
}
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -152,11 +145,7 @@ export default class QueryEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
@@ -177,7 +166,7 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full"
aria-label="Query Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>

View File

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

View File

@@ -6,7 +6,8 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
thead,
td {
border: 1px solid #efefef;
}
@@ -27,18 +28,18 @@ const Wrapper = styled.div`
}
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,136 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const handleAddParam = () => {
dispatch(
addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
param.name = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
break;
}
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateQueryParam({
param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParam = (param) => {
dispatch(
deleteQueryParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
+&nbsp;<span>Add Param</span>
</button>
</StyledWrapper>
);
};
export default QueryParams;

View File

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

View File

@@ -3,43 +3,48 @@ import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const HttpMethodSelector = ({method, onMethodSelect}) => {
const HttpMethodSelector = ({ method, onMethodSelect }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
<div className="flex-grow font-medium">{method}</div>
<div><IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2}/></div>
<div>
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>
</div>
);
});
const handleMethodSelect = (verb) => onMethodSelect(verb);
const Verb = ({verb}) => {
const Verb = ({ verb }) => {
return (
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
handleMethodSelect(verb);
}}>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleMethodSelect(verb);
}}
>
{verb}
</div>
);
};
return(
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer method-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-start'>
<Verb verb='GET' />
<Verb verb='POST' />
<Verb verb='PUT' />
<Verb verb='DELETE' />
<Verb verb='PATCH' />
<Verb verb='OPTIONS' />
<Verb verb='HEAD' />
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-start">
<Verb verb="GET" />
<Verb verb="POST" />
<Verb verb="PUT" />
<Verb verb="DELETE" />
<Verb verb="PATCH" />
<Verb verb="OPTIONS" />
<Verb verb="HEAD" />
</Dropdown>
</div>
</StyledWrapper>

View File

@@ -4,15 +4,12 @@ const Wrapper = styled.div`
height: 2.3rem;
div.method-selector-container {
border: solid 1px var(--color-layout-border);
border-right: none;
background-color: var(--color-sidebar-background);
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
div.input-container {
border: solid 1px var(--color-layout-border);
background-color: var(--color-sidebar-background);
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;

View File

@@ -4,50 +4,55 @@ import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import HttpMethodSelector from './HttpMethodSelector';
import StyledWrapper from './StyledWrapper';
import SendSvg from 'assets/send.svg';
const QueryUrl = ({item, collection, handleRun}) => {
const QueryUrl = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const onUrlChange = (value) => {
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
dispatch(
requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
})
);
};
const onMethodSelect = (verb) => {
dispatch(updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(
updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="flex items-center">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect}/>
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full mousetrap"
type="text" value={url}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
type="text"
value={url}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={(event) => onUrlChange(event.target.value)}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" onClick={handleRun}>
<img src={SendSvg.src} style={{ width: '22px' }} />
</div>
</div>
<button
style={{backgroundColor: 'var(--color-brand)'}}
className="flex items-center h-full text-white active:bg-blue-600 font-bold text-xs px-4 py-2 ml-2 uppercase rounded shadow hover:shadow-md outline-none focus:outline-none ease-linear transition-all duration-150"
onClick={handleRun}
>
<span style={{marginLeft: 5}}>Send</span>
</button>
</StyledWrapper>
)
);
};
export default QueryUrl;

View File

@@ -8,12 +8,12 @@ const Wrapper = styled.div`
border-radius: 3px;
.dropdown-item {
padding: .2rem .6rem !important;
padding: 0.2rem 0.6rem !important;
padding-left: 1.5rem !important;
}
.label-item {
padding: .2rem .6rem !important;
.label-item {
padding: 0.2rem 0.6rem !important;
}
}

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
thead,
td {
border: 1px solid #efefef;
}
@@ -26,18 +27,18 @@ const Wrapper = styled.div`
padding: 5px;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -0,0 +1,133 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
dispatch(
addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
header.name = e.target.value;
break;
}
case 'value': {
header.value = e.target.value;
break;
}
case 'description': {
header.description = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
}
}
dispatch(
updateRequestHeader({
header: header,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveHeader = (header) => {
dispatch(
deleteRequestHeader({
headerUid: header.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
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')}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={header.enabled} className="mr-3 mousetrap" onChange={(e) => handleHeaderValueChange(e, header, 'enabled')} />
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-header select-none" onClick={addHeader}>
+ Add Header
</button>
</StyledWrapper>
);
};
export default RequestHeaders;

View File

@@ -0,0 +1,48 @@
import React, { useState, useEffect } from 'react';
import { faFolder } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import StyledWrapper from './StyledWrapper';
import Modal from 'components//Modal';
const SaveRequest = ({ items, onClose }) => {
const [showFolders, setShowFolders] = useState([]);
useEffect(() => {
setShowFolders(items || []);
}, [items]);
const handleFolderClick = (folder) => {
let subFolders = [];
if (folder.items && folder.items.length) {
for (let item of folder.items) {
if (item.items) {
subFolders.push(item);
}
}
if (subFolders.length) {
setShowFolders(subFolders);
}
}
};
return (
<StyledWrapper>
<Modal size="md" title="Save Request" confirmText="Save" cancelText="Cancel" handleCancel={onClose} handleConfirm={onClose}>
<p className="mb-2">Select a folder to save request:</p>
<div className="folder-list">
{showFolders && showFolders.length
? showFolders.map((folder) => (
<div key={folder.uid} className="folder-name" onClick={() => handleFolderClick(folder)}>
<FontAwesomeIcon className="mr-3 text-gray-500" icon={faFolder} style={{ fontSize: 20 }} />
{folder.name}
</div>
))
: null}
</div>
</Modal>
</StyledWrapper>
);
};
export default SaveRequest;

View File

@@ -2,22 +2,22 @@ import React from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const RequestNotFound = ({itemUid}) => {
const RequestNotFound = ({ itemUid }) => {
const dispatch = useDispatch();
const closeTab = () => {
dispatch(closeTabs({
tabUids: [itemUid]
}));
dispatch(
closeTabs({
tabUids: [itemUid]
})
);
};
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 yml file associated with this request was deleted on your filesystem.</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab

View File

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

View File

@@ -1,21 +1,23 @@
import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import toast from 'react-hot-toast';
import { useSelector, useDispatch } from 'react-redux';
import QueryUrl from 'components/RequestPane/QueryUrl';
import GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';
import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
import ResponsePane from 'components/ResponsePane';
import Welcome from 'components/Welcome';
import { findItemInCollection } from 'utils/collections';
import { sendRequest } from 'providers/ReduxStore/slices/collections';
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
const RequestTabPanel = () => {
if(typeof window == 'undefined') {
if (typeof window == 'undefined') {
return <div></div>;
}
const dispatch = useDispatch();
@@ -26,12 +28,12 @@ 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 - 5);
const [dragging, setDragging] = useState(false);
useEffect(() => {
const leftPaneWidth = (screenWidth - asideWidth)/2.2;
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
@@ -40,20 +42,22 @@ const RequestTabPanel = () => {
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if(dragging) {
if (dragging) {
e.preventDefault();
setLeftPaneWidth(e.clientX - asideWidth - 5);
setRightPaneWidth(screenWidth - (e.clientX) - 5);
setRightPaneWidth(screenWidth - e.clientX - 5);
}
};
const handleMouseUp = (e) => {
if(dragging) {
if (dragging) {
e.preventDefault();
setDragging(false);
dispatch(updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
}));
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
})
);
}
};
const handleDragbarMouseDown = (e) => {
@@ -61,9 +65,10 @@ const RequestTabPanel = () => {
setDragging(true);
};
let {
schema
} = useGraphqlSchema('https://api.spacex.land/graphql');
let schema = null;
// let {
// schema
// } = useGraphqlSchema('https://api.spacex.land/graphql');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
@@ -75,57 +80,42 @@ const RequestTabPanel = () => {
};
}, [dragging, asideWidth]);
if(!activeTabUid) {
return (
<Welcome/>
);
if (!activeTabUid) {
return <Welcome />;
}
if(!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
if(!collection || !collection.uid) {
return (
<div className="pb-4 px-4">Collection not found!</div>
);
if (!collection || !collection.uid) {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const item = findItemInCollection(collection, activeTabUid);
if(!item || !item.uid) {
return (
<RequestNotFound itemUid={activeTabUid}/>
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
}
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
);
};
const onGraphqlQueryChange = (value) => {};
const runQuery = async () => {};
const sendNetworkRequest = async () => dispatch(sendRequest(item, collection.uid));
return (
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
<div
className="px-4 pt-6 pb-4"
style={{
borderBottom: 'solid 1px var(--color-request-dragbar-background)'
}}
>
<QueryUrl
item = {item}
collection={collection}
handleRun={sendNetworkRequest}
/>
<div className="pt-4 pb-3 px-4">
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
</div>
<section className="main flex flex-grow">
<section className="request-pane mt-2">
<div
className="px-4"
style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}}
>
<section className="main flex flex-grow pb-4">
<section className="request-pane">
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
{item.type === 'graphql-request' ? (
<GraphQLRequestPane
onRunQuery={runQuery}
@@ -136,13 +126,7 @@ 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>
@@ -150,17 +134,12 @@ const RequestTabPanel = () => {
<div className="drag-request-border" />
</div>
<section className="response-pane flex-grow mt-2">
<ResponsePane
item={item}
rightPaneWidth={rightPaneWidth}
response={item.response}
isLoading={item.response && item.response.state === 'sending' ? true : false}
/>
<section className="response-pane flex-grow">
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
</section>
</section>
</StyledWrapper>
)
);
};
export default RequestTabPanel;

View File

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

View File

@@ -1,22 +1,22 @@
import React from 'react';
import { IconFolders } from '@tabler/icons';
import EnvironmentSelector from 'components/EnvironmentSelector';
import { IconFiles } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({collection}) => {
const CollectionToolBar = ({ collection }) => {
return (
<StyledWrapper>
<div className="flex items-center p-2">
<div className="flex flex-1 items-center">
<IconFolders size={18} strokeWidth={1.5}/>
<IconFiles size={18} strokeWidth={1.5} />
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<EnvironmentSelector />
<EnvironmentSelector collection={collection} />
</div>
</div>
</StyledWrapper>
)
);
};
export default CollectionToolBar;

View File

@@ -8,11 +8,13 @@ const StyledWrapper = styled.div`
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: nowrap;
}
.close-icon-container {
min-height: 20px;
min-width: 24px;
margin-left: 4px;
border-radius: 3px;
.close-icon {
@@ -23,16 +25,16 @@ const StyledWrapper = styled.div`
padding-top: 6px;
}
&:hover, &:hover .close-icon {
&:hover,
&:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
}
.has-changes-icon {
.has-changes-icon {
height: 24px;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,110 @@
import React from 'react';
import get from 'lodash/get';
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';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
};
const getMethodColor = (method = '') => {
let color = '';
method = method.toLocaleLowerCase();
switch (method) {
case 'get': {
color = 'var(--color-method-get)';
break;
}
case 'post': {
color = 'var(--color-method-post)';
break;
}
case 'put': {
color = 'var(--color-method-put)';
break;
}
case 'delete': {
color = 'var(--color-method-delete)';
break;
}
case 'patch': {
color = 'var(--color-method-patch)';
break;
}
case 'options': {
color = 'var(--color-method-options)';
break;
}
case 'head': {
color = 'var(--color-method-head)';
break;
}
}
return color;
};
const item = findItemInCollection(collection, tab.uid);
if (!item) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
</StyledWrapper>
);
}
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-baseline tab-label pl-2">
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>
<span className="text-gray-700 ml-1 tab-name" title={item.name}>
{item.name}
</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
{!item.draft ? (
<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>
) : (
<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>
)}
</div>
</StyledWrapper>
);
};
export default RequestTab;

View File

@@ -1,13 +1,12 @@
import styled from 'styled-components';
const Wrapper = styled.div`
border-bottom: 1px solid var(--color-layout-border);
border-bottom: 1px solid var(--color-request-dragbar-background);
ul {
padding: 0;
margin: 0;
display: flex;
bottom: -1px;
position: relative;
overflow: scroll;
@@ -17,11 +16,8 @@ const Wrapper = styled.div`
li {
display: inline-flex;
width: 150px;
min-width: 150px;
max-width: 150px;
border: 1px solid transparent;
border-bottom: none;
list-style: none;
padding-top: 8px;
padding-bottom: 8px;
@@ -31,20 +27,17 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
height: 38px;
margin-right: 6px;
background: #f7f7f7;
border-radius: 0;
.tab-container {
width: 100%;
border-left: 1px solid #dcdcdc;
border-right: 1px solid transparent;
}
&.active {
border-color: var(--color-layout-border);
background: #fff;
border-radius: 5px 5px 0 0;
.tab-container {
border-left: 1px solid transparent;
}
background: #e7e7e7;
font-weight: 500;
}
&.active {
@@ -53,7 +46,7 @@ const Wrapper = styled.div`
}
}
&:hover{
&:hover {
.close-icon-container .close-icon {
display: block;
}
@@ -69,6 +62,7 @@ const Wrapper = styled.div`
justify-content: center;
color: rgb(117 117 117);
position: relative;
background-color: white;
top: -1px;
> div {
@@ -98,40 +92,6 @@ const Wrapper = styled.div`
}
}
}
li.last-tab {
.tab-container {
border-right: 1px solid #dcdcdc;
}
&.active {
.tab-container {
border-right: 1px solid transparent;
}
}
}
li.active + li {
.tab-container {
border-left: 1px solid transparent;
}
}
li:first-child {
.tab-container {
border-left: 1px solid transparent;
}
}
}
&.has-chevrons {
ul {
li:first-child {
.tab-container {
border-left: 1px solid #dcdcdc;
}
}
}
}
`;

View File

@@ -21,38 +21,36 @@ const RequestTabs = () => {
const screenWidth = useSelector((state) => state.app.screenWidth);
const getTabClassname = (tab, index) => {
return classnames("request-tab select-none", {
'active': tab.uid === activeTabUid,
'last-tab': tabs && tabs.length && (index === tabs.length - 1)
return classnames('request-tab select-none', {
active: tab.uid === activeTabUid,
'last-tab': tabs && tabs.length && index === tabs.length - 1
});
};
const handleClick = (tab) => {
dispatch(focusTab({
uid: tab.uid
}));
dispatch(
focusTab({
uid: tab.uid
})
);
};
const createNewTab = () => setNewRequestModalOpen(true);
if(!activeTabUid) {
if (!activeTabUid) {
return null;
}
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(!activeTab) {
return (
<StyledWrapper>
Something went wrong!
</StyledWrapper>
);
if (!activeTab) {
return <StyledWrapper>Something went wrong!</StyledWrapper>;
}
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
const maxTablistWidth = screenWidth - leftSidebarWidth - 150;
const tabsWidth = (collectionRequestTabs.length * 150) + 34; // 34: (+)icon
const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon
const showChevrons = maxTablistWidth < tabsWidth;
const leftSlide = () => {
@@ -78,18 +76,19 @@ const RequestTabs = () => {
});
};
// Todo: Must support ephermal requests
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && <NewRequest isEphermal={true} collection={activeCollection} onClose={() => setNewRequestModalOpen(false)}/>}
{newRequestModalOpen && <NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />}
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection}/>
<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}/>
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
@@ -100,28 +99,30 @@ const RequestTabs = () => {
</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 role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5}/>
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
<li className={`select-none short-tab ${showChevrons ? '' : 'ml-1'}`} onClick={createNewTab}>
<li className="select-none short-tab" onClick={createNewTab}>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
<svg 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>

View File

@@ -0,0 +1,28 @@
import React from 'react';
const NetworkError = ({ onClose }) => {
return (
<div className="max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex bg-red-100">
<div className="flex-1 w-0 p-4">
<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>
<div className="flex">
<button
onClick={onClose}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium focus:outline-none"
>
Close
</button>
</div>
</div>
);
};
export default NetworkError;

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.overlay{
div.overlay {
position: absolute;
top: 0;
right: 0;
@@ -18,4 +18,3 @@ const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { IconRefresh } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
import StopWatch from '../../StopWatch';
import StyledWrapper from './StyledWrapper';
const ResponseLoadingOverlay = ({ item, collection }) => {
const dispatch = useDispatch();
const handleCancelRequest = () => {
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
};
return (
<StyledWrapper className="mt-4 px-3 w-full">
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 24, marginLeft: 5, marginRight: 5 }}>
<StopWatch />
</div>
</div>
<IconRefresh size={24} className="animate-spin" />
<button
onClick={handleCancelRequest}
className="mt-4 uppercase bg-gray-200 active:bg-blueGray-600 text-xs px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
type="button"
>
Cancel Request
</button>
</div>
</StyledWrapper>
);
};
export default ResponseLoadingOverlay;

View File

@@ -6,4 +6,3 @@ const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@@ -5,8 +5,8 @@ import StyledWrapper from './StyledWrapper';
const Placeholder = () => {
return (
<StyledWrapper>
<div className="text-gray-300 flex justify-center" style={{fontSize: 200}}>
<IconSend size={150} strokeWidth={1}/>
<div className="text-gray-300 flex justify-center" style={{ fontSize: 200 }}>
<IconSend size={150} strokeWidth={1} />
</div>
<div className="flex mt-4">
<div className="flex flex-1 flex-col items-end px-1">
@@ -17,7 +17,7 @@ const Placeholder = () => {
</div>
<div className="flex flex-1 flex-col px-1">
<div className="px-1 py-2">Cmd + Enter</div>
<div className="px-1 py-2">Cmd + N</div>
<div className="px-1 py-2">Cmd + B</div>
<div className="px-1 py-2">Cmd + E</div>
<div className="px-1 py-2">Cmd + H</div>
</div>

View File

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

View File

@@ -2,11 +2,11 @@ import React from 'react';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
const QueryResult = ({value, width}) => {
const QueryResult = ({ value, width }) => {
return (
<StyledWrapper className="px-3 w-full" style={{maxWidth: width}}>
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
<div className="h-full">
<CodeEditor value={value || ''} readOnly/>
<CodeEditor value={value || ''} readOnly />
</div>
</StyledWrapper>
);

View File

@@ -0,0 +1,30 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const ResponseHeaders = ({ headers }) => {
return (
<StyledWrapper className="px-3 pb-4 w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
return (
<tr key={index}>
<td className="key">{header[0]}</td>
<td className="value">{header[1]}</td>
</tr>
);
})
: null}
</tbody>
</table>
</StyledWrapper>
);
};
export default ResponseHeaders;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const ResponseSize = ({ size }) => {
let sizeToDisplay = '';
if (size > 1024) {
// size is greater than 1kb
let kb = Math.floor(size / 1024);
let decimal = ((size % 1024) / 1024).toFixed(2) * 100;
sizeToDisplay = kb + '.' + decimal + 'KB';
} else {
sizeToDisplay = size + 'B';
}
return <StyledWrapper className="ml-4">{sizeToDisplay}</StyledWrapper>;
};
export default ResponseSize;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const ResponseTime = ({ duration }) => {
let durationToDisplay = '';
if (duration > 1000) {
// duration greater than a second
let seconds = Math.floor(duration / 1000);
let decimal = ((duration % 1000) / 1000) * 100;
durationToDisplay = seconds + '.' + decimal.toFixed(0) + 's';
} else {
durationToDisplay = duration + 'ms';
}
return <StyledWrapper className="ml-4">{durationToDisplay}</StyledWrapper>;
};
export default ResponseTime;

View File

@@ -41,7 +41,7 @@ const statusCodePhraseMap = {
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m a teapot',
418: "I'm a teapot",
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',

View File

@@ -3,7 +3,7 @@ import classnames from 'classnames';
import statusCodePhraseMap from './get-status-code-phrase';
import StyledWrapper from './StyledWrapper';
const StatusCode = ({status}) => {
const StatusCode = ({ status }) => {
const getTabClassname = () => {
return classnames('', {
'text-blue-700': status >= 100 && status < 200,
@@ -18,6 +18,6 @@ const StatusCode = ({status}) => {
<StyledWrapper className={getTabClassname()}>
{status} {statusCodePhraseMap[status]}
</StyledWrapper>
)
);
};
export default StatusCode;
export default StatusCode;

View File

@@ -10,7 +10,11 @@ const StyledWrapper = styled.div`
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
@@ -23,4 +27,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -12,34 +12,30 @@ import ResponseTime from './ResponseTime';
import ResponseSize from './ResponseSize';
import StyledWrapper from './StyledWrapper';
const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
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 selectTab = (tab) => {
dispatch(updateResponsePaneTab({
uid: item.uid,
responsePaneTab: tab
}))
dispatch(
updateResponsePaneTab({
uid: item.uid,
responsePaneTab: tab
})
);
};
const response = item.response || {};
const getTabPanel = (tab) => {
switch(tab) {
switch (tab) {
case 'response': {
return (
<QueryResult
width={rightPaneWidth}
value={response.data ? JSON.stringify(response.data, null, 2) : ''}
/>
);
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
}
case 'headers': {
return (
<ResponseHeaders headers={response.headers}/>
);
return <ResponseHeaders headers={response.headers} />;
}
default: {
@@ -48,14 +44,15 @@ const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
}
};
if(isLoading) {
if (isLoading) {
return (
<StyledWrapper className="flex h-full relative">
<Overlay />
<Overlay item={item} collection={collection} />
</StyledWrapper>
);
}
if(response.state !== 'success') {
if (response.state !== 'success') {
return (
<StyledWrapper className="flex h-full relative">
<Placeholder />
@@ -63,43 +60,41 @@ const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
);
}
if(!activeTabUid) {
return (
<div>Something went wrong</div>
);
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
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>
);
if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === focusedTab.responsePaneTab
active: tabName === focusedTab.responsePaneTab
});
};
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('response')} role="tab" onClick={() => selectTab('response')}>
Response
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
<StatusCode status={response.status}/>
<ResponseTime duration={response.duration}/>
<ResponseSize size={response.size}/>
<StatusCode status={response.status} />
<ResponseTime duration={response.duration} />
<ResponseSize size={response.size} />
</div>
) : null }
) : null}
</div>
<section className="flex flex-grow mt-5">
{getTabPanel(focusedTab.responsePaneTab)}
</section>
<section className="flex flex-grow mt-5">{getTabPanel(focusedTab.responsePaneTab)}</section>
</StyledWrapper>
)
);
};
export default ResponsePane;

View File

@@ -4,22 +4,19 @@ import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections';
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
const CloneCollectionItem = ({collection, item, onClose}) => {
const CloneCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
enableReinitialize: true,
initialValues: {
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));
@@ -28,7 +25,7 @@ const CloneCollectionItem = ({collection, item, onClose}) => {
});
useEffect(() => {
if(inputRef && inputRef.current) {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
@@ -36,27 +33,26 @@ 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">{isFolder ? 'Folder' : 'Request'} Name</label>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name" type="text" name="name"
id="collection-item-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>

View File

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

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