mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
241 Commits
bugfix/col
...
v0.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd39e630d | ||
|
|
ef8e8bf637 | ||
|
|
7405fa9709 | ||
|
|
242fcac2d3 | ||
|
|
efd15838aa | ||
|
|
c55f9d42da | ||
|
|
2f32f7024e | ||
|
|
aff6499478 | ||
|
|
45ed47ff90 | ||
|
|
27c6c1349a | ||
|
|
c78ffa3a80 | ||
|
|
6b2d335ade | ||
|
|
837e39d870 | ||
|
|
67643c4c48 | ||
|
|
2b384656b6 | ||
|
|
411c06f4cb | ||
|
|
03fa46d8b3 | ||
|
|
d0f2eb27bc | ||
|
|
1b9ec05a58 | ||
|
|
3f74178c81 | ||
|
|
78ca6c5e96 | ||
|
|
5f59a16090 | ||
|
|
3805cef0c4 | ||
|
|
3c1a6ca71e | ||
|
|
d2227b2b05 | ||
|
|
dc03b6a761 | ||
|
|
e22f164cbc | ||
|
|
580d681e0a | ||
|
|
89b721d726 | ||
|
|
1110a4edda | ||
|
|
f69332d9c3 | ||
|
|
6947860204 | ||
|
|
963b0c257f | ||
|
|
33f8900705 | ||
|
|
22a14aa67a | ||
|
|
60c96f7d27 | ||
|
|
c8de57aa51 | ||
|
|
827c480689 | ||
|
|
1c869013c6 | ||
|
|
404a516fef | ||
|
|
e26075060e | ||
|
|
c524f40ab2 | ||
|
|
3e563ea126 | ||
|
|
a0cb53445f | ||
|
|
84bd603e11 | ||
|
|
c3236d4eb1 | ||
|
|
4a4208f272 | ||
|
|
d24f1a1054 | ||
|
|
86200a8f11 | ||
|
|
cf0ede1a83 | ||
|
|
342a39bcb4 | ||
|
|
e7d332c7d7 | ||
|
|
689d886e74 | ||
|
|
7a8e5198ff | ||
|
|
118ceacf46 | ||
|
|
a21615a5fb | ||
|
|
2ee2e270b0 | ||
|
|
9d6ba4691c | ||
|
|
104bd272f9 | ||
|
|
62a184c386 | ||
|
|
4663a1246c | ||
|
|
0efd782bcb | ||
|
|
a0903a5842 | ||
|
|
8202182074 | ||
|
|
ee4d4e3361 | ||
|
|
b76ddcd007 | ||
|
|
6f6dedbb9c | ||
|
|
37b1c043eb | ||
|
|
58bc247c53 | ||
|
|
c5b509115a | ||
|
|
524a59aed4 | ||
|
|
be49ef5f12 | ||
|
|
d4f05fa843 | ||
|
|
adedd08e8a | ||
|
|
7dd0d10a5d | ||
|
|
d9ef1692fe | ||
|
|
b88848f0dc | ||
|
|
abc26e5c5a | ||
|
|
d7733552bf | ||
|
|
6852cc6631 | ||
|
|
8bfb2591c2 | ||
|
|
05a290839b | ||
|
|
80f9e33be5 | ||
|
|
5a78dfa210 | ||
|
|
61caca59ee | ||
|
|
383c5ba782 | ||
|
|
28fbaa3470 | ||
|
|
27dcf78e73 | ||
|
|
25883b84fa | ||
|
|
667811cbd4 | ||
|
|
7839e93a57 | ||
|
|
11c60273b4 | ||
|
|
1dcff56c78 | ||
|
|
2e32423869 | ||
|
|
c328281f21 | ||
|
|
cc261326fc | ||
|
|
050ee2680f | ||
|
|
b2c28465e9 | ||
|
|
cd36335c60 | ||
|
|
d89f12c071 | ||
|
|
905f459ed0 | ||
|
|
b800055df4 | ||
|
|
b1d2b798ba | ||
|
|
4a403a253e | ||
|
|
a45628dd85 | ||
|
|
977637e556 | ||
|
|
3d63db806d | ||
|
|
1ec24d1138 | ||
|
|
fa40685a6a | ||
|
|
037013005f | ||
|
|
0c42298ce6 | ||
|
|
84ce75263b | ||
|
|
5c8d0a9e8a | ||
|
|
b70bbf78b1 | ||
|
|
43b9412ddb | ||
|
|
b56972fd93 | ||
|
|
c102ac527a | ||
|
|
45229b1af7 | ||
|
|
f9a3fb2f1b | ||
|
|
65d8a707d8 | ||
|
|
cc6bf45d5f | ||
|
|
8fbb777665 | ||
|
|
0e041d460c | ||
|
|
2e3b296021 | ||
|
|
405b50edcd | ||
|
|
fff540010e | ||
|
|
e513694912 | ||
|
|
d01cada16c | ||
|
|
dd4fecfd1c | ||
|
|
095d7c6bcb | ||
|
|
d165a04377 | ||
|
|
d3d1e47950 | ||
|
|
9c14941c15 | ||
|
|
1627f65bd7 | ||
|
|
19f4f3c1a5 | ||
|
|
ae70680ceb | ||
|
|
60fc13c765 | ||
|
|
60c3d41c8e | ||
|
|
fb8ff37d83 | ||
|
|
0d9b30e730 | ||
|
|
695f42df80 | ||
|
|
6b43159be2 | ||
|
|
21c9c8b4fb | ||
|
|
c4abe54c3f | ||
|
|
dd71c9e71b | ||
|
|
2be3e4bf69 | ||
|
|
f34e9f7b26 | ||
|
|
76b0729af3 | ||
|
|
4877bc3849 | ||
|
|
0742e3415c | ||
|
|
ae7e3a722c | ||
|
|
7f2e19250f | ||
|
|
4e16e954ef | ||
|
|
b6c3205474 | ||
|
|
23076b41c6 | ||
|
|
b5116b54af | ||
|
|
83aaa21b5b | ||
|
|
e1e7b37ce5 | ||
|
|
8dab9268f2 | ||
|
|
4eed999db1 | ||
|
|
c29ab50a3d | ||
|
|
5e1d6cba4a | ||
|
|
a645d1459c | ||
|
|
24e11a864c | ||
|
|
87a4778a91 | ||
|
|
0750af4c68 | ||
|
|
60e613fac8 | ||
|
|
b75baf57ba | ||
|
|
137df3c5c0 | ||
|
|
6ef2daebbd | ||
|
|
55f85e3728 | ||
|
|
f0269069d2 | ||
|
|
61dbca3243 | ||
|
|
f21cb240c4 | ||
|
|
ca46e14732 | ||
|
|
87f6000b85 | ||
|
|
36d0550472 | ||
|
|
ee4734c957 | ||
|
|
02f9fc0a7b | ||
|
|
6ce657d891 | ||
|
|
cffef31f97 | ||
|
|
b93be5a846 | ||
|
|
544765af3e | ||
|
|
2393092248 | ||
|
|
dcdeb78995 | ||
|
|
e16650d4a7 | ||
|
|
62ed489847 | ||
|
|
2930eb29ec | ||
|
|
eecb60f5cf | ||
|
|
82fb2819c2 | ||
|
|
2aef7c61a4 | ||
|
|
530af1f929 | ||
|
|
3753fd1e20 | ||
|
|
a59ae75809 | ||
|
|
4c18c27406 | ||
|
|
5d25fdcf7a | ||
|
|
8cfdb3ebcb | ||
|
|
9e64ea5439 | ||
|
|
23c8044973 | ||
|
|
46ac15dd81 | ||
|
|
5ad9be4f6b | ||
|
|
f46625c689 | ||
|
|
c0e1bf6bc2 | ||
|
|
874ca07f39 | ||
|
|
a291e7f345 | ||
|
|
c9cabfde35 | ||
|
|
fde15d7c31 | ||
|
|
11defe18ca | ||
|
|
54b14a005d | ||
|
|
f283df2a1b | ||
|
|
820c99711b | ||
|
|
df1cd4aff9 | ||
|
|
481486cd1c | ||
|
|
bf4c26de33 | ||
|
|
c3fa473dae | ||
|
|
90a29918d0 | ||
|
|
c0698adcb3 | ||
|
|
0d0f99e810 | ||
|
|
7f5a6d5566 | ||
|
|
dc68d511bd | ||
|
|
0fceaf6918 | ||
|
|
831223711a | ||
|
|
e4cf3750bd | ||
|
|
01e15b7fc1 | ||
|
|
3bf3d30ce8 | ||
|
|
bf6e6b29f5 | ||
|
|
075aaaebec | ||
|
|
1136f1b105 | ||
|
|
5c8e66b684 | ||
|
|
09faf46635 | ||
|
|
ef28637d0c | ||
|
|
51784d08cd | ||
|
|
96f50b0c6d | ||
|
|
2ba6e4823d | ||
|
|
04a0a37ca4 | ||
|
|
23400a77f8 | ||
|
|
4718c77e3d | ||
|
|
0cde789697 | ||
|
|
6be2818bfb | ||
|
|
8e70e191e1 | ||
|
|
cbdfabb4db |
27
.github/workflows/playwright.yml
vendored
Normal file
27
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:e2e
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
23
.github/workflows/unit-tests.yml
vendored
Normal file
23
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,6 +17,7 @@ chrome-extension
|
||||
chrome-extension.pem
|
||||
chrome-extension.crx
|
||||
bruno.zip
|
||||
*.zip
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -37,3 +38,6 @@ yarn-error.log*
|
||||
/renderer
|
||||
/renderer/.next/
|
||||
/renderer/out/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
BIN
assets/images/landing-2.png
Normal file
BIN
assets/images/landing-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
BIN
assets/images/local-collections.png
Normal file
BIN
assets/images/local-collections.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
@@ -1,28 +1,31 @@
|
||||
## Lets make bruno better, together !!
|
||||
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computed.
|
||||
|
||||
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
|
||||
Libraries we use
|
||||
* CSS - Tailwind
|
||||
* Code Editors - Codemirror
|
||||
* State Management - Redux
|
||||
* Icons - Tabler Icons
|
||||
* Forms - formik
|
||||
* Schema Validation - Yup
|
||||
* Request Client - axios
|
||||
* Filesystem Watcher - chokidar
|
||||
Libraries we use
|
||||
|
||||
- CSS - Tailwind
|
||||
- Code Editors - Codemirror
|
||||
- State Management - Redux
|
||||
- Icons - Tabler Icons
|
||||
- Forms - formik
|
||||
- Schema Validation - Yup
|
||||
- Request Client - axios
|
||||
- Filesystem Watcher - chokidar
|
||||
|
||||
### Dependencies
|
||||
You would need Node v14.x and npm 8.x. We use npm workspaces in the project
|
||||
|
||||
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
|
||||
### Lets start coding
|
||||
|
||||
```bash
|
||||
# clone and cd into bruno
|
||||
# use Node 14.x, Npm 8.x
|
||||
# use Node 14.x, Npm 8.x
|
||||
|
||||
# Install deps (note that we use npm workspaces)
|
||||
npm i
|
||||
@@ -41,4 +44,10 @@ open http://localhost:3000
|
||||
```
|
||||
|
||||
### Raising Pull Request
|
||||
* Please keep the PR's small and focused on one thing
|
||||
|
||||
- Please keep the PR's small and focused on one thing
|
||||
- Please follow the format of creating branches
|
||||
- feature/[feature name]: This branch should contain changes for a specific feature
|
||||
- Example: feature/dark-mode
|
||||
- bugfix/[bug name]: This branch should container only bug fixes for a specific bug
|
||||
- Example bugfix/bug-1
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
## development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
###
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
|
||||
@@ -14,14 +24,17 @@ npm run dev --workspace=packages/bruno-electron
|
||||
npm run build --workspace=packages/bruno-app
|
||||
```
|
||||
|
||||
## fix
|
||||
### fix
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
# testing
|
||||
### testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
```
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
const { ipcMain } = require('electron');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'Collection',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Collection',
|
||||
click () {
|
||||
ipcMain.emit('main:open-collection');
|
||||
}
|
||||
},
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo'},
|
||||
{ role: 'redo'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'cut'},
|
||||
{ role: 'copy'},
|
||||
{ role: 'paste'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload'},
|
||||
{ role: 'toggledevtools'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'resetzoom'},
|
||||
{ role: 'zoomin'},
|
||||
{ role: 'zoomout'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'togglefullscreen'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{ role: 'minimize'},
|
||||
{ role: 'close'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{ label: 'Learn More'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = template;
|
||||
@@ -1,54 +0,0 @@
|
||||
const path = require('path');
|
||||
const { format } = require('url');
|
||||
const { BrowserWindow, app, Menu } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
const registerIpc = require('./ipc');
|
||||
const isDev = require('electron-is-dev');
|
||||
const prepareNext = require('electron-next');
|
||||
|
||||
setContentSecurityPolicy(`
|
||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src * 'unsafe-inline';
|
||||
base-uri 'none';
|
||||
form-action 'none';
|
||||
frame-ancestors 'none';
|
||||
`);
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
let mainWindow;
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
await prepareNext('./renderer');
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 768,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js")
|
||||
},
|
||||
});
|
||||
|
||||
const url = isDev
|
||||
? 'http://localhost:8000'
|
||||
: format({
|
||||
pathname: path.join(__dirname, '../renderer/out/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
});
|
||||
|
||||
mainWindow.loadURL(url);
|
||||
|
||||
// register all ipc handlers
|
||||
registerIpc(mainWindow);
|
||||
});
|
||||
|
||||
// Quit the app once all windows are closed
|
||||
app.on('window-all-closed', app.quit);
|
||||
47
main/ipc.js
47
main/ipc.js
@@ -1,47 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend } = require('lodash');
|
||||
|
||||
|
||||
const registerIpc = () => {
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, request) => {
|
||||
try {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
const form = new FormData();
|
||||
forOwn(request.data, (value, key) => {
|
||||
form.append(key, value);
|
||||
});
|
||||
extend(request.headers, form.getHeaders());
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
const result = await axios(request);
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
headers: result.headers,
|
||||
data: result.data
|
||||
};
|
||||
} catch (error) {
|
||||
if(error.response) {
|
||||
return {
|
||||
status: error.response.status,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: -1,
|
||||
headers: [],
|
||||
data: null
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = registerIpc;
|
||||
@@ -1,14 +0,0 @@
|
||||
const { ipcRenderer, contextBridge } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
on: (channel, handler) => {
|
||||
// Deliberately strip event as it includes `sender`
|
||||
const subscription = (event, ...args) => handler(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
const { customAlphabet } = require('nanoid');
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
const uuid = () => {
|
||||
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
|
||||
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
|
||||
const customNanoId = customAlphabet (urlAlphabet, 21);
|
||||
|
||||
return customNanoId();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uuid
|
||||
};
|
||||
17
package.json
17
package.json
@@ -4,19 +4,32 @@
|
||||
"workspaces": [
|
||||
"packages/bruno-app",
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-testbench"
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"jest": "^29.2.0",
|
||||
"randomstring": "^1.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh"
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"paths": {
|
||||
"assets/*": ["src/assets/*"],
|
||||
"components/*": ["src/components/*"],
|
||||
"hooks/*": ["src/hooks/*"],
|
||||
"themes/*": ["src/themes/*"],
|
||||
"api/*": ["src/api/*"],
|
||||
"pageComponents/*": ["src/pageComponents/*"],
|
||||
"providers/*": ["src/providers/*"],
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT,
|
||||
ENV: process.env.ENV
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
if (!isServer) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "cross-env ENV=dev next dev",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,7 +17,8 @@
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/schema": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -25,21 +28,27 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.1",
|
||||
"next": "12.3.3",
|
||||
"path": "^0.12.7",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tabs": "^3.2.3",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
@@ -53,6 +62,7 @@
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"babel-loader": "^8.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^3.0.1",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 436 KiB |
30
packages/bruno-app/public/theme/dark.js
Normal file
30
packages/bruno-app/public/theme/dark.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const darkTheme = {
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'primary-text': '#ffffff',
|
||||
'primary-theme': '#1e1e1e',
|
||||
'secondary-text': '#929292',
|
||||
'sidebar-collection-item-active-indent-border': '#d0d0d0',
|
||||
'sidebar-collection-item-active-background': '#e1e1e1',
|
||||
'sidebar-background': '#252526',
|
||||
'sidebar-bottom-bg': '#68217a',
|
||||
'request-dragbar-background': '#efefef',
|
||||
'request-dragbar-background-active': 'rgb(200, 200, 200)',
|
||||
'tab-inactive': 'rgb(155 155 155)',
|
||||
'tab-active-border': '#546de5',
|
||||
'layout-border': '#dedede',
|
||||
'codemirror-border': '#efefef',
|
||||
'codemirror-background': 'rgb(243, 243, 243)',
|
||||
'text-link': '#1663bb',
|
||||
'text-danger': 'rgb(185, 28, 28)',
|
||||
'background-danger': '#dc3545',
|
||||
'method-get': 'rgb(5, 150, 105)',
|
||||
'method-post': '#8e44ad',
|
||||
'method-delete': 'rgb(185, 28, 28)',
|
||||
'method-patch': 'rgb(52 52 52)',
|
||||
'method-options': 'rgb(52 52 52)',
|
||||
'method-head': 'rgb(52 52 52)',
|
||||
'table-stripe': '#f3f3f3'
|
||||
};
|
||||
|
||||
export default darkTheme;
|
||||
7
packages/bruno-app/public/theme/index.js
Normal file
7
packages/bruno-app/public/theme/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import darkTheme from './dark';
|
||||
import lightTheme from './light';
|
||||
|
||||
export default {
|
||||
Light: lightTheme,
|
||||
Dark: darkTheme
|
||||
};
|
||||
30
packages/bruno-app/public/theme/light.js
Normal file
30
packages/bruno-app/public/theme/light.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const lightTheme = {
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'primary-text': 'rgb(52 52 52)',
|
||||
'primary-theme': '#ffffff',
|
||||
'secondary-text': '#929292',
|
||||
'sidebar-collection-item-active-indent-border': '#d0d0d0',
|
||||
'sidebar-collection-item-active-background': '#e1e1e1',
|
||||
'sidebar-background': '#f3f3f3',
|
||||
'sidebar-bottom-bg': '#f3f3f3',
|
||||
'request-dragbar-background': '#efefef',
|
||||
'request-dragbar-background-active': 'rgb(200, 200, 200)',
|
||||
'tab-inactive': 'rgb(155 155 155)',
|
||||
'tab-active-border': '#546de5',
|
||||
'layout-border': '#dedede',
|
||||
'codemirror-border': '#efefef',
|
||||
'codemirror-background': 'rgb(243, 243, 243)',
|
||||
'text-link': '#1663bb',
|
||||
'text-danger': 'rgb(185, 28, 28)',
|
||||
'background-danger': '#dc3545',
|
||||
'method-get': 'rgb(5, 150, 105)',
|
||||
'method-post': '#8e44ad',
|
||||
'method-delete': 'rgb(185, 28, 28)',
|
||||
'method-patch': 'rgb(52 52 52)',
|
||||
'method-options': 'rgb(52 52 52)',
|
||||
'method-head': 'rgb(52 52 52)',
|
||||
'table-stripe': '#f3f3f3'
|
||||
};
|
||||
|
||||
export default lightTheme;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { get, post, put } from './base';
|
||||
|
||||
// not used. kept as a placeholder for reference while implementing license key stuff
|
||||
const AuthApi = {
|
||||
whoami: () => get('auth/v1/user/whoami'),
|
||||
signup: (params) => post('auth/v1/user/signup', params),
|
||||
login: (params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer
|
||||
.invoke('bruno-account-request', {
|
||||
data: params,
|
||||
method: 'POST',
|
||||
url: `${process.env.NEXT_PUBLIC_BRUNO_SERVER_API}/auth/v1/user/login`
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default AuthApi;
|
||||
@@ -1,30 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
headers: headers
|
||||
};
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
return Promise.reject(error.response ? error.response.data : error);
|
||||
}
|
||||
);
|
||||
|
||||
const { get, post, put, delete: destroy } = apiClient;
|
||||
|
||||
export { get, post, put, destroy };
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: var(--color-text);
|
||||
color: ${(props) => props.theme.text};
|
||||
.collection-options {
|
||||
svg {
|
||||
position: relative;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconSpeakerphone, IconBrandTwitter } from '@tabler/icons';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GithubSvg from 'assets/github.svg';
|
||||
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
return (
|
||||
@@ -10,19 +9,25 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-center">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<img src={GithubSvg.src} style={{ width: '18px' }} />
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Github</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-center">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandTwitter size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Twitter</span>
|
||||
</a>
|
||||
|
||||
@@ -2,12 +2,44 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
border: solid 1px var(--color-codemirror-border);
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
@@ -15,7 +18,7 @@ if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class QueryEditor extends React.Component {
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,6 +26,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -38,7 +42,9 @@ export default class QueryEditor extends React.Component {
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: "overlay",
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
@@ -60,6 +66,8 @@ export default class QueryEditor extends React.Component {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
Tab: function (cm) {
|
||||
cm.replaceSelection(' ', 'end');
|
||||
}
|
||||
@@ -67,6 +75,7 @@ export default class QueryEditor extends React.Component {
|
||||
}));
|
||||
if (editor) {
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +94,17 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if(this.editor) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
@@ -109,6 +128,15 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
const mode = this.props.mode || 'application/ld+json';
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
|
||||
@@ -9,11 +9,11 @@ const Wrapper = styled.div`
|
||||
|
||||
.tippy-box {
|
||||
min-width: 135px;
|
||||
background-color: white;
|
||||
font-size: 0.8125rem;
|
||||
color: rgb(48 48 48);
|
||||
background: #fff;
|
||||
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
border-radius: 3px;
|
||||
|
||||
.tippy-content {
|
||||
padding-left: 0;
|
||||
@@ -25,6 +25,7 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background-color: ${(props) => props.theme.dropdown.labelBg};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@@ -33,8 +34,16 @@ const Wrapper = styled.div`
|
||||
padding: 0.35rem 0.6rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e9e9e9;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-enviroment {
|
||||
background: #efefef;
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
|
||||
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -63,13 +63,14 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<span>No Environment</span>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className='ml-2'>No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item" style={{ borderTop: 'solid 1px #e7e7e7' }} onClick={() => setOpenSettingsModal(true)}>
|
||||
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Settings</span>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@ const Wrapper = styled.div`
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid #efefef;
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #616161;
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -7,7 +7,6 @@ import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
console.log(environment);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
|
||||
@@ -4,8 +4,11 @@ const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.environments-sidebar {
|
||||
background-color: #eaeaea;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@@ -20,15 +23,15 @@ const StyledWrapper = styled.div`
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #e4e4e4;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #dcdcdc !important;
|
||||
border-left: solid 2px var(--color-brand);
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: #dcdcdc !important;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +39,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: var(--color-text-link);
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -9,8 +10,13 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEnvironment(environments[0]);
|
||||
}, []);
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if(environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [collection, environments]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
|
||||
17
packages/bruno-app/src/components/Icons/Send/index.js
Normal file
17
packages/bruno-app/src/components/Icons/Send/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const SendIcon = ({color, width}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z"/>
|
||||
<path d="M0 0h48v48h-48z" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SendIcon;
|
||||
@@ -66,8 +66,8 @@ const Wrapper = styled.div`
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
color: rgb(86 86 86);
|
||||
background-color: #f1f1f1;
|
||||
color: ${(props) => props.theme.modal.title.color};
|
||||
background-color: ${(props) => props.theme.modal.title.bg};
|
||||
font-size: 0.75rem;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
@@ -77,7 +77,7 @@ const Wrapper = styled.div`
|
||||
.close {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1;
|
||||
color: #000;
|
||||
color: ${(props) => props.theme.modal.iconColor};
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: 0.5;
|
||||
margin-top: -2px;
|
||||
@@ -90,7 +90,30 @@ const Wrapper = styled.div`
|
||||
|
||||
.bruno-modal-content {
|
||||
flex-grow: 1;
|
||||
background-color: #fff;
|
||||
background-color: ${(props) => props.theme.modal.body.bg};
|
||||
|
||||
.textbox {
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out .1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bruno-form {
|
||||
color: ${(props) => props.theme.modal.body.color};
|
||||
}
|
||||
}
|
||||
|
||||
.bruno-modal-backdrop {
|
||||
@@ -107,7 +130,7 @@ const Wrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
opacity: 0.4;
|
||||
opacity: ${(props) => props.theme.modal.backdrop.opacity};
|
||||
top: 0;
|
||||
background: black;
|
||||
position: fixed;
|
||||
@@ -117,10 +140,17 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.bruno-modal-footer {
|
||||
background-color: white;
|
||||
background-color: ${(props) => props.theme.modal.body.bg};
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
&.modal-footer-none {
|
||||
.bruno-modal-content {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -64,6 +64,9 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
|
||||
if (isClosing) {
|
||||
classes += ' modal--animate-out';
|
||||
}
|
||||
if(hideFooter) {
|
||||
classes += ' modal-footer-none';
|
||||
}
|
||||
return (
|
||||
<StyledWrapper className={classes}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
|
||||
@@ -5,19 +5,28 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid #efefef;
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #616161;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +38,8 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +23,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.react-tabs__tab-list {
|
||||
border-bottom: none !important;
|
||||
padding-top: 0;
|
||||
padding-left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
|
||||
.react-tabs__tab {
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
user-select: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 20px;
|
||||
color: rgb(125 125 125);
|
||||
outline: none !important;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@@ -27,36 +19,12 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: none !important;
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
border: none;
|
||||
color: #322e2c !important;
|
||||
border-bottom: solid 2px var(--color-tab-active-border) !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
border: none;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-bottom: solid 2px var(--color-tab-active-border) !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,26 +1,159 @@
|
||||
import React from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import React, { useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
|
||||
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||
const variables = item.draft ? get(item, 'draft.request.body.graphql.variables') : get(item, 'request.body.graphql.variables');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url, environment);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
loadSchema();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(onSchemaLoad) {
|
||||
onSchemaLoad(schema);
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
const onQueryChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
value={query}
|
||||
onRun={onRun}
|
||||
onEdit={onQueryChange}
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>;
|
||||
}
|
||||
case 'variables': {
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occured!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full">
|
||||
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
|
||||
<TabList>
|
||||
<Tab tabIndex="-1">Query</Tab>
|
||||
<Tab tabIndex="-1">Headers</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<div className="mt-4">
|
||||
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
|
||||
Variables
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? (
|
||||
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
|
||||
) : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<RequestHeaders />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div
|
||||
className='flex items-center cursor-pointer hover:underline ml-2'
|
||||
onClick={toggleDocs}
|
||||
>
|
||||
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [schema, setSchema] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if(!saved) {
|
||||
return null;
|
||||
}
|
||||
return buildClientSchema(JSON.parse(saved));
|
||||
} catch {
|
||||
localStorage.setItem(localStorageKey, null);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('Graphql Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading Graphql Schema');
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schema,
|
||||
loadSchema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={variables || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLVariables;
|
||||
@@ -20,8 +20,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #322e2c !important;
|
||||
border-bottom: solid 2px var(--color-tab-active-border) !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
@@ -34,6 +36,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -67,6 +75,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
@@ -75,7 +89,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className={`flex w-full ${focusedTab.requestPaneTab === 'script' ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,19 +5,28 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid #efefef;
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #616161;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +38,9 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +23,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
value={param.value}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -2,14 +2,46 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
border: solid 1px var(--color-codemirror-border);
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import MD from 'markdown-it';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
@@ -29,6 +32,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -37,11 +41,17 @@ export default class QueryEditor extends React.Component {
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
// mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection),
|
||||
},
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
scrollbarStyle: "overlay",
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
foldGutter: {
|
||||
minFoldSize: 4
|
||||
@@ -75,54 +85,51 @@ export default class QueryEditor extends React.Component {
|
||||
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-C': () => {
|
||||
if (this.props.onCopyQuery) {
|
||||
this.props.onCopyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-P': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
|
||||
|
||||
'Shift-Ctrl-F': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-M': () => {
|
||||
if (this.props.onMergeQuery) {
|
||||
this.props.onMergeQuery();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
@@ -131,6 +138,7 @@ export default class QueryEditor extends React.Component {
|
||||
editor.on('hasCompletion', this._onHasCompletion);
|
||||
editor.on('beforeChange', this._onBeforeChange);
|
||||
}
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -149,6 +157,15 @@ export default class QueryEditor extends React.Component {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -161,10 +178,20 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: Overlay is messing up with schema hint
|
||||
// Fix this
|
||||
addOverlay = () => {
|
||||
// let variables = getAllVariables(this.props.collection);
|
||||
// this.variables = variables;
|
||||
|
||||
// defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
// this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full"
|
||||
className="h-full w-full"
|
||||
aria-label="Query Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
@@ -173,8 +200,11 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_onKeyUp = (_cm, event) => {
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
|
||||
_onKeyUp = (_cm, e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
|
||||
this.editor.execCommand('autocomplete');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,19 +5,28 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid #efefef;
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #616161;
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +41,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,12 +3,16 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
|
||||
const handleAddParam = () => {
|
||||
@@ -20,6 +24,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
|
||||
@@ -32,10 +38,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -68,7 +70,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -90,27 +91,17 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -10,7 +10,7 @@ const HttpMethodSelector = ({ method, onMethodSelect }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
|
||||
<div className="flex-grow font-medium">{method}</div>
|
||||
<div className="flex-grow font-medium" id="create-new-request-method">{method}</div>
|
||||
<div>
|
||||
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,18 @@ const Wrapper = styled.div`
|
||||
height: 2.3rem;
|
||||
|
||||
div.method-selector-container {
|
||||
background-color: var(--color-sidebar-background);
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
div.input-container {
|
||||
background-color: var(--color-sidebar-background);
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
input {
|
||||
background-color: var(--color-sidebar-background);
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect} from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SendSvg from 'assets/send.svg';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme, storedTheme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector(".method-selector-container");
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@@ -36,19 +48,24 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
className="px-3 w-full mousetrap"
|
||||
type="text"
|
||||
value={url}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={(event) => onUrlChange(event.target.value)}
|
||||
<div
|
||||
className="flex items-center flex-grow input-container h-full"
|
||||
style={{
|
||||
color: 'yellow',
|
||||
width: `calc(100% - ${methodSelectorWidth}px)`,
|
||||
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
|
||||
}}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" onClick={handleRun}>
|
||||
<img src={SendSvg.src} style={{ width: '22px' }} />
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.body-mode-selector {
|
||||
background: #efefef;
|
||||
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
|
||||
@@ -4,6 +4,7 @@ import CodeEditor from 'components/CodeEditor';
|
||||
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
|
||||
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -12,6 +13,9 @@ const RequestBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -41,7 +45,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
<CodeEditor collection={collection} theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,32 +5,40 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid #efefef;
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #616161;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-header {
|
||||
font-size: 0.8125rem;
|
||||
margin-block: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -19,6 +23,8 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
header.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={header.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, header, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -124,7 +115,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header select-none" onClick={addHeader}>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: rgb(155 155 155);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1'>
|
||||
<div className='mb-1 title'>Request</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 mt-6'>
|
||||
<div className='mt-1 mb-1 title'>Response</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
45
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
45
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestTests({
|
||||
tests: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={tests || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const RequestNotFound = ({ itemUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = () => {
|
||||
dispatch(
|
||||
@@ -13,11 +14,25 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||
<div>Request no longer exists.</div>
|
||||
<div className="mt-2">This can happen when the yml file associated with this request was deleted on your filesystem.</div>
|
||||
<div className="mt-2">This can happen when the .bru file associated with this request was deleted on your filesystem.</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
Close Tab
|
||||
|
||||
@@ -18,11 +18,30 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
border-left: solid 1px var(--color-request-dragbar-background);
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
|
||||
}
|
||||
|
||||
&:hover div.drag-request-border {
|
||||
border-left: solid 1px var(--color-request-dragbar-background-active);
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
|
||||
div.graphql-docs-explorer-container {
|
||||
background: white;
|
||||
outline: none;
|
||||
box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
z-index: 2000;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
|
||||
div.doc-explorer-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.doc-explorer-rhs {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
@@ -12,10 +12,15 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
const DEFAULT_PADDING = 5;
|
||||
|
||||
const RequestTabPanel = () => {
|
||||
if (typeof window == 'undefined') {
|
||||
return <div></div>;
|
||||
@@ -29,23 +34,43 @@ const RequestTabPanel = () => {
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
// Not a recommended pattern here to have the child component
|
||||
// make a callback to set state, but treating this as an exception
|
||||
const docExplorerRef = useRef(null);
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [showGqlDocs, setShowGqlDocs] = useState(false);
|
||||
const onSchemaLoad = (schema) => setSchema(schema);
|
||||
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
|
||||
const handleGqlClickReference = (reference) => {
|
||||
if(docExplorerRef.current) {
|
||||
docExplorerRef.current.showDocForReference(reference);
|
||||
}
|
||||
if(!showGqlDocs) {
|
||||
setShowGqlDocs(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
|
||||
setLeftPaneWidth(leftPaneWidth);
|
||||
}, [screenWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - 5);
|
||||
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
}, [screenWidth, asideWidth, leftPaneWidth]);
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
setLeftPaneWidth(e.clientX - asideWidth - 5);
|
||||
setRightPaneWidth(screenWidth - e.clientX - 5);
|
||||
let leftPaneXPosition = e.clientX + 2;
|
||||
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
|
||||
return;
|
||||
}
|
||||
setLeftPaneWidth(leftPaneXPosition- asideWidth);
|
||||
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
|
||||
}
|
||||
};
|
||||
const handleMouseUp = (e) => {
|
||||
@@ -55,7 +80,7 @@ const RequestTabPanel = () => {
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - asideWidth - 5
|
||||
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -65,11 +90,6 @@ const RequestTabPanel = () => {
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
let schema = null;
|
||||
// let {
|
||||
// schema
|
||||
// } = useGraphqlSchema('https://api.spacex.land/graphql');
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@@ -93,6 +113,11 @@ const RequestTabPanel = () => {
|
||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
const showRunner = collection.showRunner;
|
||||
if(showRunner) {
|
||||
return <RunnerResults collection={collection}/>;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
@@ -105,24 +130,23 @@ const RequestTabPanel = () => {
|
||||
})
|
||||
);
|
||||
};
|
||||
const onGraphqlQueryChange = (value) => {};
|
||||
const runQuery = async () => {};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
</div>
|
||||
<section className="main flex flex-grow pb-4">
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section className="request-pane">
|
||||
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
|
||||
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
|
||||
{item.type === 'graphql-request' ? (
|
||||
<GraphQLRequestPane
|
||||
onRunQuery={runQuery}
|
||||
schema={schema}
|
||||
item={item}
|
||||
collection={collection}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
value={item.request.body.graphql.query}
|
||||
onQueryChange={onGraphqlQueryChange}
|
||||
onSchemaLoad={onSchemaLoad}
|
||||
toggleDocs={toggleDocs}
|
||||
handleGqlClickReference={handleGqlClickReference}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -138,8 +162,22 @@ const RequestTabPanel = () => {
|
||||
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{item.type === 'graphql-request' ? (
|
||||
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
|
||||
<DocExplorer schema={schema} ref={(r) => docExplorerRef.current = r}>
|
||||
<button
|
||||
className='mr-2'
|
||||
onClick={toggleDocs}
|
||||
aria-label="Close Documentation Explorer"
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
</DocExplorer>
|
||||
</div>
|
||||
): null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabPanel;
|
||||
export default RequestTabPanel;
|
||||
@@ -1,9 +1,20 @@
|
||||
import React from 'react';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { IconFiles, IconRun } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import VariablesView from 'components/VariablesView';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionToolBar = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(toggleRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
@@ -12,6 +23,10 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
<VariablesView collection={collection}/>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
|
||||
const RequestTabNotFound = ({handleCloseClick}) => {
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
{showErrorMessage ? (
|
||||
<>
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Not Found</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabNotFound;
|
||||
@@ -19,7 +19,7 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
color: #9f9f9f;
|
||||
color: ${(props) => props.theme.requestTabs.icon.color};
|
||||
width: 8px;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
@@ -27,8 +27,8 @@ const StyledWrapper = styled.div`
|
||||
|
||||
&:hover,
|
||||
&:hover .close-icon {
|
||||
background-color: #eaeaea;
|
||||
color: rgb(76 76 76);
|
||||
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
|
||||
}
|
||||
|
||||
.has-changes-icon {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -61,18 +61,7 @@ const RequestTab = ({ tab, collection }) => {
|
||||
if (!item) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Not Found</span>
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +74,7 @@ const RequestTab = ({ tab, collection }) => {
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
||||
{method}
|
||||
</span>
|
||||
<span className="text-gray-700 ml-1 tab-name" title={item.name}>
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-bottom: 1px solid var(--color-request-dragbar-background);
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.borromBorder};
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
@@ -28,7 +28,8 @@ const Wrapper = styled.div`
|
||||
height: 38px;
|
||||
|
||||
margin-right: 6px;
|
||||
background: #f7f7f7;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-radius: 0;
|
||||
|
||||
.tab-container {
|
||||
@@ -36,7 +37,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e7e7e7;
|
||||
background: ${(props) => props.theme.requestTabs.active.bg};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -60,9 +61,9 @@ const Wrapper = styled.div`
|
||||
padding: 3px 0px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
color: rgb(117 117 117);
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.color};
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.bg};
|
||||
position: relative;
|
||||
background-color: white;
|
||||
top: -1px;
|
||||
|
||||
> div {
|
||||
@@ -85,8 +86,8 @@ const Wrapper = styled.div`
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: #eaeaea;
|
||||
color: rgb(76 76 76);
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg};
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.hoverColor};
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -76,6 +76,8 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showRunner = activeCollection && activeCollection.showRunner;
|
||||
|
||||
// Todo: Must support ephermal requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
@@ -83,59 +85,61 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
{!showRunner ? (
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,9 +7,6 @@ const NetworkError = ({ onClose }) => {
|
||||
<div className="flex items-start">
|
||||
<div className="ml-3 flex-1">
|
||||
<p className="text-sm font-medium text-red-800">Network Error</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS. If not, please use the chrome extension or the desktop app
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
<IconRefresh size={24} className="animate-spin" />
|
||||
<button
|
||||
onClick={handleCancelRequest}
|
||||
className="mt-4 uppercase bg-gray-200 active:bg-blueGray-600 text-xs px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||
className="mt-4 uppercase btn-md rounded btn-secondary ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
>
|
||||
Cancel Request
|
||||
|
||||
@@ -3,6 +3,10 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
padding-top: 20%;
|
||||
width: 100%;
|
||||
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -5,7 +5,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const Placeholder = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="text-gray-300 flex justify-center" style={{ fontSize: 200 }}>
|
||||
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
|
||||
<IconSend size={150} strokeWidth={1} />
|
||||
</div>
|
||||
<div className="flex mt-4">
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import React from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = ({ value, width }) => {
|
||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener }) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRun = () => {
|
||||
if(disableRunEventListener) {
|
||||
return;
|
||||
}
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor value={value || ''} readOnly />
|
||||
<CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value || ''} readOnly />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ const Wrapper = styled.div`
|
||||
|
||||
tbody {
|
||||
tr:nth-child(odd) {
|
||||
background-color: var(--color-table-stripe);
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgb(117 117 117);
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -3,7 +3,7 @@ import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgb(117 117 117);
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -3,6 +3,14 @@ import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.text-ok {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseOk};
|
||||
}
|
||||
|
||||
&.text-error {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseError};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -3,19 +3,20 @@ import classnames from 'classnames';
|
||||
import statusCodePhraseMap from './get-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Todo: text-error class is not getting pulled in for 500 errors
|
||||
const StatusCode = ({ status }) => {
|
||||
const getTabClassname = () => {
|
||||
const getTabClassname = (status) => {
|
||||
return classnames('', {
|
||||
'text-blue-700': status >= 100 && status < 200,
|
||||
'text-green-700': status >= 200 && status < 300,
|
||||
'text-purple-700': status >= 300 && status < 400,
|
||||
'text-yellow-700': status >= 400 && status < 500,
|
||||
'text-red-700': status >= 500 && status < 600
|
||||
'text-ok': status >= 100 && status < 200,
|
||||
'text-ok': status >= 200 && status < 300,
|
||||
'text-error': status >= 300 && status < 400,
|
||||
'text-error': status >= 400 && status < 500,
|
||||
'text-error': status >= 500 && status < 600
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getTabClassname()}>
|
||||
<StyledWrapper className={getTabClassname(status)}>
|
||||
{status} {statusCodePhraseMap[status]}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -20,11 +20,19 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #322e2c !important;
|
||||
border-bottom: solid 2px var(--color-tab-active-border) !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TestResults = ({ results }) => {
|
||||
if (!results || !results.length) {
|
||||
return (
|
||||
<div className="px-3">
|
||||
No tests found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className='flex flex-col px-3'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.description}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResults;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
const TestResultsLabel = ({ results }) => {
|
||||
if(!results || !results.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
const numberOfTests = results.length;
|
||||
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div>Tests</div>
|
||||
{numberOfFailedTests ? (
|
||||
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||
{numberOfFailedTests}
|
||||
</sup>
|
||||
) : (
|
||||
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||
{numberOfTests}
|
||||
</sup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResultsLabel;
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Timeline = ({ request, response}) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = response.headers || [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = safeStringifyJSON(request.data);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 pb-4 w-full">
|
||||
<div>
|
||||
<pre className='line request font-bold'>
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line request' key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className='line request'>
|
||||
<span className="arrow">{'>'}</span> data {requestData}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<pre className='line response font-bold'>
|
||||
<span className="arrow">{'<'}</span> {response.status} {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line response' key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
@@ -10,13 +11,16 @@ import ResponseHeaders from './ResponseHeaders';
|
||||
import StatusCode from './StatusCode';
|
||||
import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = item.response && item.response.state === 'sending';
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -32,11 +36,22 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? safeStringifyJSON(response.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={item.requestSent} response={item.response}/>;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
@@ -84,6 +99,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} />
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={response.status} />
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const {
|
||||
requestSent,
|
||||
responseReceived,
|
||||
testResults
|
||||
} = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', {});
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
|
||||
const selectTab = (tab) => setSelectedTab(tab);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
disableRunEventListener={true}
|
||||
value={(responseReceived && responseReceived.data) ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
<ResponseTime duration={duration} />
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsePane;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.item-path {
|
||||
.link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
.danger {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.test-summary {
|
||||
color: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
|
||||
/* test results */
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
197
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
197
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir, name } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
}
|
||||
|
||||
export default function RunnerResults({collection}) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if(!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
item.name = info.name;
|
||||
item.type = info.type;
|
||||
item.filename = info.filename;
|
||||
item.pathname = info.pathname;
|
||||
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||
|
||||
if(item.status !== "error") {
|
||||
if(item.testResults) {
|
||||
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.testStatus = 'pass';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
};
|
||||
|
||||
const closeRunner = () => {
|
||||
dispatch(closeCollectionRunner({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass');
|
||||
const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail');
|
||||
|
||||
if(!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
You have <span className='font-medium'>{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 mb-4 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='flex flex-col flex-1'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== "error" && item.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
<span className={`mr-1 ml-2 ${(item.status == "error" || item.testStatus == 'fail') ? 'danger' : ''}`}>{item.relativePath}</span>
|
||||
{(item.status !== "error" && item.status !== "completed") ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<span className='text-xs link cursor-pointer' onClick={() => setSelectedItem(item)}>
|
||||
(<span className='mr-1'>
|
||||
{get(item.responseReceived, 'status')}
|
||||
</span>
|
||||
<span>
|
||||
{get(item.responseReceived, 'statusText')}
|
||||
</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.status == "error" ? (
|
||||
<div className="error-message pl-8 pt-2 text-xs">
|
||||
{item.error}
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.testResults ? item.testResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)): null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='flex flex-1' style={{width: '50%'}}>
|
||||
{selectedItem ? (
|
||||
<div className='flex flex-col w-full overflow-auto'>
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className='mr-2'>{selectedItem.relativePath}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
|
||||
<ResponsePane item={selectedItem} collection={collection}/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -19,8 +20,13 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occured while cloning the request')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,25 +13,25 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.method-get {
|
||||
color: var(--color-method-get);
|
||||
color: ${(props) => props.theme.request.methods.get};
|
||||
}
|
||||
.method-post {
|
||||
color: var(--color-method-post);
|
||||
color: ${(props) => props.theme.request.methods.post};
|
||||
}
|
||||
.method-put {
|
||||
color: var(--color-method-put);
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
}
|
||||
.method-delete {
|
||||
color: var(--color-method-delete);
|
||||
color: ${(props) => props.theme.request.methods.delete};
|
||||
}
|
||||
.method-patch {
|
||||
color: var(--color-method-patch);
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
}
|
||||
.method-options {
|
||||
color: var(--color-method-options);
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
}
|
||||
.method-head {
|
||||
color: var(--color-method-head);
|
||||
color: ${(props) => props.theme.request.methods.put};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.bruno-modal-content {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(showRunnerView({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
|
||||
const items = flattenItems(item ? item.items : collection.items);
|
||||
const requestItems = items.filter((item) => item.type !== 'folder');
|
||||
const recursiveRunLength = requestItems.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title='Collection Runner' hideFooter={true} handleCancel={onClose}>
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Run</span>
|
||||
<span className='ml-1 text-xs'>({runLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will only run the requests in this folder.
|
||||
</div>
|
||||
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Recursive Run</span>
|
||||
<span className='ml-1 text-xs'>({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will run all the requests in this folder and all its subfolders.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className='mr-3'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunCollectionItem;
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.menu-icon {
|
||||
color: rgb(110 110 110);
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
.dropdown {
|
||||
div[aria-expanded='true'] {
|
||||
@@ -15,7 +15,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.indent-block {
|
||||
border-right: solid 1px #e1e1e1;
|
||||
border-right: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
}
|
||||
|
||||
.collection-item-name {
|
||||
@@ -34,7 +34,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #e7e7e7;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
.menu-icon {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
@@ -45,14 +45,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&.item-focused-in-tab {
|
||||
background: var(--color-sidebar-collection-item-active-background);
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg};
|
||||
|
||||
&:hover {
|
||||
background: var(--color-sidebar-collection-item-active-background) !important;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
|
||||
}
|
||||
|
||||
.indent-block {
|
||||
border-right: solid 1px var(--color-sidebar-collection-item-active-indent-border);
|
||||
border-right: ${(props) => props.theme.sidebar.collection.item.active.indentBorder} !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
div.dropdown-item.delete-item {
|
||||
color: var(--color-text-danger);
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: var(--color-background-danger);
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging .collection-item-name {
|
||||
&.is-sidebar-dragging .collection-item-name {
|
||||
cursor: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -13,8 +15,10 @@ import RequestMethod from './RequestMethod';
|
||||
import RenameCollectionItem from './RenameCollectionItem';
|
||||
import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -22,7 +26,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isDragging = useSelector((state) => state.app.isDragging);
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
@@ -30,8 +34,32 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `COLLECTION_ITEM_${collection.uid}`,
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
}
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setItemisCollapsed(false);
|
||||
@@ -58,6 +86,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(hideRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
if (isItemARequest(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
@@ -69,7 +100,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -89,7 +121,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-dragging': isDragging
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
@@ -104,8 +136,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestItems = filter(item.items, (i) => isItemARequest(i));
|
||||
const folderItems = filter(item.items, (i) => isItemAFolder(i));
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
@@ -114,7 +156,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
||||
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
||||
<div className={itemRowClassName}>
|
||||
{runCollectionModalOpen && <RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />}
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
@@ -174,6 +217,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setRunCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
@@ -212,13 +264,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
{!itemIsCollapsed ? (
|
||||
<div>
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user