Compare commits

..

2 Commits

Author SHA1 Message Date
Anusree Subash
6ee0a3e1e8 chore: fixed lint issues 2022-10-16 17:57:25 +05:30
Anusree Subash
890c79b013 feat: added support links 2022-10-16 17:55:27 +05:30
424 changed files with 28511 additions and 18986 deletions

View File

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

View File

@@ -1,29 +0,0 @@
name: Unit Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Build Package bruno-query
run: npm run build --workspace=packages/bruno-query
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js

11
.gitignore vendored
View File

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

2
.nvmrc
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

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

View File

@@ -1,53 +1,18 @@
## Development
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
### Dependencies
* NodeJS v18
### Local Development
## development
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
npm i
# build graphql docs
# note: you can for now ignore the error thrown while building the graphql docs
npm run build:graphql-docs
# run next app
npm run dev --workspace=packages/bruno-app
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
# run electron app
npm run dev --workspace=packages/bruno-electron
```
### Troubleshooting
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Testing
# testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
```

55
main/app/menu-template.js Normal file
View File

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

54
main/index.js Normal file
View File

@@ -0,0 +1,54 @@
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 Normal file
View File

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

14
main/preload.js Normal file
View File

@@ -0,0 +1,14 @@
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);
};
}
});

14
main/utils/common.js Normal file
View File

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

20737
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +4,12 @@
"workspaces": [
"packages/bruno-app",
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
"packages/bruno-testbench"
],
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"jest": "^29.2.0",
"randomstring": "^1.2.2",
"ts-jest": "^29.0.5"
},
"scripts": {
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:electron": "./scripts/build-electron.sh",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report"
},
"overrides": {
"rollup": "3.2.5"
"randomstring": "^1.2.2"
}
}
}

View File

@@ -4,9 +4,6 @@
node_modules
.pnp
.pnp.js
pnpm-lock.yaml
package-lock.json
yarn.lock
# testing
coverage

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,11 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "cross-env ENV=dev next dev",
"build": "next build && next export",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
@@ -17,8 +14,6 @@
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.3.1",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -28,28 +23,23 @@
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"graphiql": "^1.5.9",
"graphql": "^16.6.0",
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",
"next": "^12.1.0",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-github-btn": "^1.4.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.4.0",
"react-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"react-tabs": "^3.2.3",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"yup": "^0.32.11"
@@ -61,13 +51,11 @@
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.7.1",
"style-loader": "^3.3.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

View File

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 211 B

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,44 +2,13 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
}
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
background: #d2d7db;
border: solid 1px var(--color-codemirror-border);
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom{
color: #569cd6 !important;
}
.cm-variable-valid{color: green}
.cm-variable-invalid{color: red}
`;
export default StyledWrapper;

View File

@@ -6,9 +6,6 @@
*/
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;
@@ -18,7 +15,7 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
export default class CodeEditor extends React.Component {
export default class QueryEditor extends React.Component {
constructor(props) {
super(props);
@@ -26,7 +23,6 @@ export default class CodeEditor 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() {
@@ -41,41 +37,36 @@ export default class CodeEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly,
scrollbarStyle: "overlay",
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
readOnly: this.props.readOnly ? 'nocursor' : false,
extraKeys: {
'Cmd-Enter': () => {
if (this.props.onRun) {
if(this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onRun) {
if(this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if (this.props.onSave) {
if(this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
if(this.props.onSave) {
this.props.onSave();
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
'Tab': function(cm){
cm.replaceSelection(" " , "end");
}
}
},
}));
if (editor) {
editor.on('change', this._onEdit);
this.addOverlay();
}
}
@@ -91,20 +82,14 @@ export default class CodeEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
if(this.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.editor.setOption("mode", this.props.mode);
}
this.ignoreChangeEvent = false;
}
@@ -121,22 +106,13 @@ export default class CodeEditor extends React.Component {
<StyledWrapper
className="h-full"
aria-label="Code Editor"
ref={(node) => {
ref={node => {
this._node = node;
}}
/>
);
}
addOverlay = () => {
const mode = this.props.mode || 'application/ld+json';
let variables = getEnvironmentVariables(this.props.collection);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode);
this.editor.setOption('mode', 'brunovariables');
}
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();

View File

@@ -9,11 +9,11 @@ const Wrapper = styled.div`
.tippy-box {
min-width: 135px;
background-color: white;
font-size: 0.8125rem;
color: ${(props) => props.theme.dropdown.color};
background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.dropdown.shadow};
border-radius: 3px;
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;
.tippy-content {
padding-left: 0;
@@ -24,26 +24,17 @@ const Wrapper = styled.div`
.label-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
background-color: ${(props) => props.theme.dropdown.labelBg};
padding: .35rem .6rem;
}
.dropdown-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
padding: .35rem .6rem;
cursor: pointer;
.icon {
color: ${(props) => props.theme.dropdown.iconColor};
}
&:hover {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
background-color: #e9e9e9;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,25 @@
import React, { useState } from 'react';
import { IconEdit, IconTrash, IconDatabase } from '@tabler/icons';
import EnvironmentVariables from './EnvironmentVariables';
import RenameEnvironment from '../../RenameEnvironment';
import DeleteEnvironment from '../../DeleteEnvironment';
import React, {useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons";
import RenameEnvironment from "../../RenameEnvironment";
import DeleteEnvironment from "../../DeleteEnvironment";
const EnvironmentDetails = ({ environment, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const EnvironmentDetails = ({environment, collection}) => {
const [ openEditModal, setOpenEditModal] = useState(false);
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />}
<div className="flex">
<div className="flex flex-grow items-center">
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
<span className="ml-1 font-semibold">{environment.name}</span>
<div className="ml-6 flex-grow flex pt-6" style={{maxWidth: '700px'}}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
<div className="flex flex-grow">
<div className="flex-grow font-medium">{environment.name}</div>
<div className="flex gap-x-4 pl-4 pr-6">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
</div>
</div>
<div>
<EnvironmentVariables key={environment.uid} environment={environment} collection={collection} />
</div>
</div>
);
};

View File

@@ -1,14 +1,11 @@
import styled from 'styled-components';
import styled from "styled-components";
const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.collection.environment.settings.bg};
.environments-sidebar {
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
background-color: #eaeaea;
min-height: 400px;
}
@@ -20,18 +17,18 @@ const StyledWrapper = styled.div`
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
background-color: #e4e4e4;
}
}
.active {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
background-color: #dcdcdc !important;
border-left: solid 2px var(--color-brand);
&:hover {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
background-color: #dcdcdc !important;
}
}
@@ -39,7 +36,7 @@ const StyledWrapper = styled.div`
padding: 8px 10px;
cursor: pointer;
border-bottom: none;
color: ${(props) => props.theme.textLink};
color: var(--color-text-link);
&:hover {
span {
@@ -49,4 +46,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,73 +1,42 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react';
import { findEnvironmentInCollection } from 'utils/collections';
import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment/index';
import StyledWrapper from './StyledWrapper';
import React, { useEffect, useState, forwardRef, useRef } from "react";
import EnvironmentDetails from "./EnvironmentDetails";
import CreateEnvironment from "../CreateEnvironment/index";
import StyledWrapper from "./StyledWrapper";
const EnvironmentList = ({ collection }) => {
const EnvironmentList = ({collection}) => {
const { environments } = collection;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
const envUids = environments ? environments.map((env) => env.uid) : [];
const prevEnvUids = usePrevious(envUids);
useEffect(() => {
if(selectedEnvironment) {
return;
}
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if(environment) {
setSelectedEnvironment(environment);
} else {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}
}, [collection, environments, selectedEnvironment]);
setSelectedEnvironment(environments[0]);
}, []);
useEffect(() => {
// check env add
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
if(newEnv){
setSelectedEnvironment(newEnv);
}
}
// check env delete
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}
}, [envUids, environments, prevEnvUids]);
if (!selectedEnvironment) {
if(!selectedEnvironment) {
return null;
}
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)}/>}
<div className="flex">
<div>
<div className="environments-sidebar">
{environments &&
environments.length &&
environments.map((env) => (
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span>
</div>
))}
{environments && environments.length && environments.map((env) => (
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? "environment-item active": "environment-item"}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span>
</div>
))}
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
+ <span>Create</span>
</div>
</div>
</div>
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
<EnvironmentDetails environment={selectedEnvironment} collection={collection}/>
</div>
</StyledWrapper>
);

View File

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

View File

@@ -1,4 +1,4 @@
import styled from 'styled-components';
import styled from "styled-components";
const StyledWrapper = styled.div`
button.btn-create-environment {
@@ -10,4 +10,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

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

View File

@@ -1,17 +0,0 @@
import React from 'react';
const SendIcon = ({color, width}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 48 48"
>
<path fill={color} d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z"/>
<path d="M0 0h48v48h-48z" fill="none"/>
</svg>
);
}
export default SendIcon;

View File

@@ -1,11 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div`
&.modal--animate-out {
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
&.modal--animate-out{
animation: fade-out 0.5s forwards cubic-bezier(.19,1,.22,1);
.bruno-modal-card {
animation: fade-and-slide-out-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
animation: fade-and-slide-out-from-top .50s forwards cubic-bezier(.19,1,.22,1);
}
}
@@ -23,8 +23,8 @@ const Wrapper = styled.div`
}
.bruno-modal-card {
animation-duration: 0.85s;
animation-delay: 0.1s;
animation-duration: .85s;
animation-delay: .1s;
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
box-shadow: var(--box-shadow-base);
display: flex;
flex-direction: column;
will-change: opacity, transform;
will-change: opacity,transform;
flex-grow: 0;
margin: 3vh 10vw;
margin-top: 50px;
@@ -58,7 +58,7 @@ const Wrapper = styled.div`
max-width: calc(100% - 30px);
}
animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
animation: fade-and-slide-in-from-top .50s forwards cubic-bezier(.19,1,.22,1);
}
.bruno-modal-header {
@@ -66,8 +66,8 @@ const Wrapper = styled.div`
justify-content: space-between;
align-items: center;
text-transform: uppercase;
color: ${(props) => props.theme.modal.title.color};
background-color: ${(props) => props.theme.modal.title.bg};
color: rgb(86 86 86);
background-color: #f1f1f1;
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: ${(props) => props.theme.modal.iconColor};
color: #000;
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
margin-top: -2px;
@@ -90,30 +90,7 @@ const Wrapper = styled.div`
.bruno-modal-content {
flex-grow: 1;
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};
}
background-color: #fff;
}
.bruno-modal-backdrop {
@@ -125,32 +102,25 @@ const Wrapper = styled.div`
will-change: opacity;
background: transparent;
&:before {
content: '';
&:before{
content: "";
height: 100%;
width: 100%;
left: 0;
opacity: ${(props) => props.theme.modal.backdrop.opacity};
opacity: .4;
top: 0;
background: black;
position: fixed;
}
animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
animation: fade-in .1s forwards cubic-bezier(.19,1,.22,1);
}
.bruno-modal-footer {
background-color: ${(props) => props.theme.modal.body.bg};
background-color: white;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&.modal-footer-none {
.bruno-modal-content {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
`;
export default Wrapper;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import React, {useState, useEffect} from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel }) => (
const ModalHeader = ({title, handleCancel}) => (
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
{handleCancel ? (
@@ -12,9 +12,13 @@ const ModalHeader = ({ title, handleCancel }) => (
</div>
);
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
const ModalContent = ({children}) => (
<div className="bruno-modal-content px-4 py-6">
{children}
</div>
);
const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel, hideFooter }) => {
const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confirmDisabled, hideCancel, hideFooter}) => {
confirmText = confirmText || 'Save';
cancelText = cancelText || 'Cancel';
@@ -24,21 +28,32 @@ const ModalFooter = ({ confirmText, cancelText, handleSubmit, handleCancel, conf
return (
<div className="flex justify-end p-4 bruno-modal-footer">
<span className={hideCancel ? 'hidden' : 'mr-2'}>
<span className={hideCancel ? "hidden" : "mr-2"}>
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
{cancelText}
</button>
</span>
<span>
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit}>
<button type="submit" className="submit btn btn-md btn-secondary" disabled={confirmDisabled} onClick={handleSubmit} >
{confirmText}
</button>
</span>
</div>
);
};
}
const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfirm, children, confirmDisabled, hideCancel, hideFooter }) => {
const Modal = ({
size,
title,
confirmText,
cancelText,
handleCancel,
handleConfirm,
children,
confirmDisabled,
hideCancel,
hideFooter
}) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
const escKeyCode = 27;
@@ -57,26 +72,23 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}
}, []);
let classes = 'bruno-modal';
if (isClosing) {
classes += ' modal--animate-out';
}
if(hideFooter) {
classes += ' modal-footer-none';
}
return (
<StyledWrapper className={classes}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal()} />
<ModalContent>{children}</ModalContent>
<ModalFooter
<ModalFooter
confirmText={confirmText}
cancelText={cancelText}
handleCancel={() => closeModal()}
handleSubmit={handleConfirm}
handleCancel={() => closeModal()}
handleSubmit={handleConfirm}
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
hideFooter={hideFooter}

View File

@@ -14,6 +14,7 @@ const StyledWrapper = styled.div`
user-select: none;
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

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

View File

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

View File

@@ -1,68 +0,0 @@
import React from 'react';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const AssertionOperator = ({ operator, onChange }) => {
const operators = [
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'length', 'matches', 'notMatches',
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
const handleChange = (e) => {
onChange(e.target.value);
};
const getLabel = (operator) => {
switch(operator) {
case 'eq':
return 'equals';
case 'neq':
return 'notEquals';
default:
return operator;
}
};
return (
<select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => (
<option key={operator} value={operator}>
{getLabel(operator)}
</option>
))}
</select>
);
};
export default AssertionOperator;

View File

@@ -1,162 +0,0 @@
import React from 'react';
import { IconTrash } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import AssertionOperator from '../AssertionOperator';
import { useTheme } from 'providers/Theme';
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const parseAssertionOperator = (str = '') => {
if(!str || typeof str !== 'string' || !str.length) {
return {
operator: 'eq',
value: str
};
}
const operators = [
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
'contains', 'notContains', 'length', 'matches', 'notMatches',
'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
const unaryOperators = [
'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
const [operator, ...rest] = str.trim().split(' ');
const value = rest.join(' ');
if(unaryOperators.includes(operator)) {
return {
operator,
value: ''
};
}
if(operators.includes(operator)) {
return {
operator,
value
};
}
return {
operator: 'eq',
value: str
};
};
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
];
return unaryOperators.includes(operator);
};
const AssertionRow = ({
item, collection, assertion, handleAssertionChange, handleRemoveAssertion,
onSave, handleRun
}) => {
const { storedTheme } = useTheme();
const {
operator,
value
} = parseAssertionOperator(assertion.value);
return (
<tr key={assertion.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<td>
<AssertionOperator
operator={operator}
onChange={(op) => handleAssertionChange({
target: {
value: `${op} ${value}`
}
}, assertion, 'value')}
/>
</td>
<td>
{!isUnaryOperator(operator) ? (
<SingleLineEditor
value={value}
theme={storedTheme}
readOnly={true}
onSave={onSave}
onChange={(newValue) => handleAssertionChange({
target: {
value: newValue
}
}, assertion, 'value')}
onRun={handleRun}
collection={collection}
/>
) : (
<input
type="text"
className='cursor-default'
disabled
/>
)}
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={assertion.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
/>
<button onClick={() => handleRemoveAssertion(assertion)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
};
export default AssertionRow;

View File

@@ -1,56 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(4) {
width: 70px;
}
}
}
.btn-add-assertion {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -1,96 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { useDispatch } from 'react-redux';
import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import AssertionRow from './AssertionRow';
import StyledWrapper from './StyledWrapper';
const Assertions = ({ item, collection }) => {
const dispatch = useDispatch();
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
const handleAddAssertion = () => {
dispatch(
addAssertion({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleAssertionChange = (e, _assertion, type) => {
const assertion = cloneDeep(_assertion);
switch (type) {
case 'name': {
assertion.name = e.target.value;
break;
}
case 'value': {
assertion.value = e.target.value;
break;
}
case 'enabled': {
assertion.enabled = e.target.checked;
break;
}
}
dispatch(
updateAssertion({
assertion: assertion,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveAssertion = (assertion) => {
dispatch(
deleteAssertion({
assertUid: assertion.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Expr</td>
<td>Operator</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{assertions && assertions.length
? assertions.map((assertion) => {
return (
<AssertionRow
key={assertion.uid}
assertion={assertion}
item={item}
collection={collection}
handleAssertionChange={handleAssertionChange}
handleRemoveAssertion={handleRemoveAssertion}
onSave={onSave}
handleRun={handleRun}
/>
);
})
: null}
</tbody>
</table>
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
+ Add Assertion
</button>
</StyledWrapper>
);
};
export default Assertions;

View File

@@ -5,28 +5,18 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
thead, td {
border: 1px solid #efefef;
}
thead {
color: ${(props) => props.theme.table.thead.color};
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
@@ -34,20 +24,18 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
input[type='text'] {
input[type="text"] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
&:focus{
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
input[type="checkbox"] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -3,63 +3,55 @@ 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 FormUrlEncodedParams = ({item, collection}) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
const addParam = () => {
dispatch(
addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
switch(type) {
case 'name' : {
param.name = e.target.value;
break;
}
case 'value': {
case 'value' : {
param.value = e.target.value;
break;
}
case 'enabled': {
case 'description' : {
param.description = e.target.value;
break;
}
case 'enabled' : {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateFormUrlEncodedParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateFormUrlEncodedParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
const handleRemoveParams = (param) => {
dispatch(
deleteFormUrlEncodedParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(deleteFormUrlEncodedParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="w-full">
<table>
@@ -67,58 +59,61 @@ const FormUrlEncodedParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={param.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleParamChange({
target: {
value: newValue
}
}, param, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>+ Add Param</button>
</StyledWrapper>
);
)
};
export default FormUrlEncodedParams;
export default FormUrlEncodedParams;

View File

@@ -1,30 +1,55 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
div.tab {
.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 {
padding: 6px 0px;
border: none;
user-select: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
margin-right: 20px;
color: rgb(125 125 125);
outline: none !important;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
&:after {
display: none !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;
export default StyledWrapper;

View File

@@ -1,175 +1,34 @@
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 React from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import QueryEditor from 'components/RequestPane/QueryEditor';
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import { useTheme } from 'providers/Theme';
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findEnvironmentInCollection } from 'utils/collections';
import useGraphqlSchema from './useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
const variables = item.draft ? get(item, 'draft.request.body.graphql.variables') : get(item, 'request.body.graphql.variables');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const {
storedTheme
} = useTheme();
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let {
schema,
loadSchema,
isLoading: isSchemaLoading,
error: schemaError
} = useGraphqlSchema(url, environment);
const loadGqlSchema = () => {
if(!isSchemaLoading) {
loadSchema();
}
};
useEffect(() => {
if(onSchemaLoad) {
onSchemaLoad(schema);
}
}, [schema]);
const onQueryChange = (value) => {
dispatch(
updateRequestGraphqlQuery({
query: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const selectTab = (tab) => {
dispatch(
updateRequestPaneTab({
uid: item.uid,
requestPaneTab: tab
})
);
};
const getTabPanel = (tab) => {
switch (tab) {
case 'query': {
return <QueryEditor
collection={collection}
theme={storedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}
value={query}
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
/>;
}
case 'variables': {
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
}
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
case 'assert': {
return <Assertions item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;
}
case 'tests': {
return <Tests item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
}
};
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
}
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === focusedTab.requestPaneTab
});
};
const GraphQLRequestPane = ({onRunQuery, schema, leftPaneWidth, value, onQueryChange}) => {
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
Query
</div>
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
Variables
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
{isSchemaLoading ? (
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
) : null}
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
<span className='ml-1'>Schema</span>
<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}
/>
</div>
<div
className='flex items-center cursor-pointer hover:underline ml-2'
onClick={toggleDocs}
>
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
</div>
</div>
</div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
</TabPanel>
<TabPanel>
<RequestHeaders />
</TabPanel>
</Tabs>
</StyledWrapper>
);
)
};
export default GraphQLRequestPane;

View File

@@ -1,55 +0,0 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { buildClientSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const useGraphqlSchema = (endpoint, environment) => {
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [schema, setSchema] = useState(() => {
try {
const saved = localStorage.getItem(localStorageKey);
if(!saved) {
return null;
}
return buildClientSchema(JSON.parse(saved));
} catch {
localStorage.setItem(localStorageKey, null);
return null;
}
});
const loadSchema = () => {
setIsLoading(true);
fetchGqlSchema(endpoint, environment)
.then((res) => res.data)
.then((s) => {
if (s && s.data) {
setSchema(buildClientSchema(s.data));
setIsLoading(false);
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
toast.success('GraphQL Schema loaded successfully');
} else {
return Promise.reject(new Error('An error occurred while introspecting schema'));
}
})
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error('Error occured while loading GraphQL Schema');
});
};
return {
isLoading,
schema,
loadSchema,
error
};
};
export default useGraphqlSchema;

View File

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

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestGraphqlVariables({
variables: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection} value={variables || ''}
theme={storedTheme}
onEdit={onEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default GraphQLVariables;

View File

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

View File

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

View File

@@ -5,28 +5,18 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
thead, td {
border: 1px solid #efefef;
}
thead {
color: ${(props) => props.theme.table.thead.color};
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
@@ -34,21 +24,18 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
input[type='text'] {
input[type="text"] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
&:focus{
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
input[type="checkbox"] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -3,63 +3,55 @@ 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 MultipartFormParams = ({item, collection}) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
const addParam = () => {
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
switch(type) {
case 'name' : {
param.name = e.target.value;
break;
}
case 'value': {
case 'value' : {
param.value = e.target.value;
break;
}
case 'enabled': {
case 'description' : {
param.description = e.target.value;
break;
}
case 'enabled' : {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateMultipartFormParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateMultipartFormParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
const handleRemoveParams = (param) => {
dispatch(
deleteMultipartFormParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(deleteMultipartFormParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="w-full">
<table>
@@ -67,58 +59,61 @@ const MultipartFormParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) => handleParamChange({
target: {
value: newValue
}
}, param, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>+ Add Param</button>
</StyledWrapper>
);
)
};
export default MultipartFormParams;
export default MultipartFormParams;

View File

@@ -2,46 +2,15 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
border: solid 1px var(--color-codemirror-border);
/* todo: find a better way */
height: calc(100vh - 220px);
height: calc(100vh - 250px);
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number{
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom{
color: #569cd6 !important;
}
.cm-variable-valid{color: green}
.cm-variable-invalid{color: red}
`;
export default StyledWrapper;

View File

@@ -6,10 +6,7 @@
*/
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';
@@ -32,7 +29,6 @@ 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() {
@@ -41,96 +37,100 @@ export default class QueryEditor extends React.Component {
lineNumbers: true,
tabSize: 2,
mode: 'graphql',
// mode: 'brunovariables',
brunoVarInfo: {
variables: getAllVariables(this.props.collection),
},
theme: this.props.editorTheme || 'graphiql',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
scrollbarStyle: "overlay",
readOnly: this.props.readOnly ? 'nocursor' : false,
foldGutter: {
minFoldSize: 4
minFoldSize: 4,
},
lint: {
schema: this.props.schema,
validationRules: this.props.validationRules ?? null,
// linting accepts string or FragmentDefinitionNode[]
externalFragments: this.props?.externalFragments
externalFragments: this.props?.externalFragments,
},
hintOptions: {
schema: this.props.schema,
closeOnUnfocus: false,
completeSingle: false,
container: this._node,
externalFragments: this.props?.externalFragments
externalFragments: this.props?.externalFragments,
},
info: {
schema: this.props.schema,
renderDescription: (text) => md.render(text),
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference),
},
jump: {
schema: this.props.schema,
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference)
},
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: {
'Cmd-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
if (this.props.onRunQuery) {
this.props.onRunQuery();
}
},
'Ctrl-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
if (this.props.onRunQuery) {
this.props.onRunQuery();
}
},
'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.onSave) {
this.props.onSave();
return false;
if (this.props.onRunQuery) {
// empty
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
this.props.onSave();
return false;
if (this.props.onRunQuery) {
// empty
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent'
}
},
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -138,7 +138,6 @@ export default class QueryEditor extends React.Component {
editor.on('hasCompletion', this._onHasCompletion);
editor.on('beforeChange', this._onBeforeChange);
}
this.addOverlay();
}
componentDidUpdate(prevProps) {
@@ -153,19 +152,14 @@ export default class QueryEditor extends React.Component {
this.editor.options.jump.schema = this.props.schema;
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
if (
this.props.value !== prevProps.value &&
this.props.value !== this.cachedValue &&
this.editor
) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
let variables = getAllVariables(this.props.collection);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
this.addOverlay();
}
this.ignoreChangeEvent = false;
}
@@ -178,33 +172,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 w-full"
className="h-full"
aria-label="Query Editor"
ref={(node) => {
ref={node => {
this._node = node;
}}
/>
);
}
_onKeyUp = (_cm, e) => {
if (e.metaKey || e.ctrlKey || e.altKey) {
return;
}
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
_onKeyUp = (_cm, event) => {
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
this.editor.execCommand('autocomplete');
}
};

View File

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

View File

@@ -5,28 +5,18 @@ const Wrapper = styled.div`
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
thead, td {
border: 1px solid #efefef;
}
thead {
color: ${(props) => props.theme.table.thead.color};;
color: #616161;
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
@@ -37,19 +27,18 @@ const Wrapper = styled.div`
}
}
input[type='text'] {
input[type="text"] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
&:focus{
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
input[type="checkbox"] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -3,64 +3,56 @@ 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 QueryParams = ({item, collection}) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const handleAddParam = () => {
dispatch(
addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'name': {
switch(type) {
case 'name' : {
param.name = e.target.value;
break;
}
case 'value': {
case 'value' : {
param.value = e.target.value;
break;
}
case 'enabled': {
case 'description' : {
param.description = e.target.value;
break;
}
case 'enabled' : {
param.enabled = e.target.checked;
break;
}
}
dispatch(
updateQueryParam({
param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateQueryParam({
param,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
const handleRemoveParam = (param) => {
dispatch(
deleteQueryParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(deleteQueryParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
@@ -68,60 +60,65 @@ const QueryParams = ({ item, collection }) => {
<table>
<thead>
<tr>
<td>Name</td>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={param.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleParamChange({
target: {
value: newValue
}
}, param, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={param.enabled} className="mr-3 mousetrap" onChange={(e) => handleParamChange(e, param, 'enabled')} />
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
+&nbsp;<span>Add Param</span>
</button>
</StyledWrapper>
);
)
};
export default QueryParams;
export default QueryParams;

View File

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

View File

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

View File

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

View File

@@ -1,75 +1,49 @@
import React, { useState, useEffect} from 'react';
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({ item, collection, handleRun }) => {
const { theme, storedTheme } = useTheme();
const QueryUrl = ({item, collection, handleRun}) => {
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
useEffect(() => {
const el = document.querySelector(".method-selector-container");
setMethodSelectorWidth(el.offsetWidth);
}, [method]);
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onUrlChange = (value) => {
dispatch(
requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
})
);
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
};
const onMethodSelect = (verb) => {
dispatch(
updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="flex items-center">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect}/>
</div>
<div
className="flex items-center flex-grow input-container h-full"
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 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 h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
<div className="flex items-center h-full mr-2 cursor-pointer" onClick={handleRun}>
<img src='/send.svg' style={{width: '22px'}}/>
</div>
</div>
</StyledWrapper>
);
)
};
export default QueryUrl;

View File

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

View File

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

View File

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

View File

@@ -4,33 +4,27 @@ 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';
const RequestBody = ({ item, collection }) => {
const RequestBody = ({item, collection}) => {
const dispatch = useDispatch();
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestBody({
content: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateRequestBody({
content: value,
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onRun = () => dispatch(sendRequest(item, collection.uid));;
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
if (['json', 'xml', 'text'].includes(bodyMode)) {
if(['json', 'xml', 'text'].includes(bodyMode)) {
let codeMirrorMode = {
json: 'application/ld+json',
text: 'application/text',
@@ -43,21 +37,31 @@ const RequestBody = ({ item, collection }) => {
xml: body.xml
};
return (
return(
<StyledWrapper className="w-full">
<CodeEditor collection={collection} theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
<CodeEditor
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}
onSave={onSave}
mode={codeMirrorMode[bodyMode]}
/>
</StyledWrapper>
);
}
if (bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection} />;
if(bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection}/>;
}
if (bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection} />;
if(bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection}/>;
}
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
return(
<StyledWrapper className="w-full">
No Body
</StyledWrapper>
);
};
export default RequestBody;

View File

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

View File

@@ -3,122 +3,117 @@ 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 RequestHeaders = ({item, collection}) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
dispatch(
addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid,
}));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
switch(type) {
case 'name' : {
header.name = e.target.value;
break;
}
case 'value': {
case 'value' : {
header.value = e.target.value;
break;
}
case 'enabled': {
case 'description' : {
header.description = e.target.value;
break;
}
case 'enabled' : {
header.enabled = e.target.checked;
break;
}
}
dispatch(
updateRequestHeader({
header: header,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(updateRequestHeader({
header: header,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
const handleRemoveHeader = (header) => {
dispatch(
deleteRequestHeader({
headerUid: header.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(deleteRequestHeader({
headerUid: header.uid,
itemUid: item.uid,
collectionUid: collection.uid
}));
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Key</td>
<td>Value</td>
<td>Description</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={header.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleHeaderValueChange({
target: {
value: newValue
}
}, header, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={header.enabled} className="mr-3 mousetrap" onChange={(e) => handleHeaderValueChange(e, header, 'enabled')} />
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
{headers && headers.length ? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={header.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
</tbody>
</table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
<button className="btn-add-header select-none" onClick={addHeader}>+ Add Header</button>
</StyledWrapper>
);
)
};
export default RequestHeaders;
export default RequestHeaders;

View File

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

View File

@@ -1,13 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: inherit;
}
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -1,70 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Script = ({ item, collection }) => {
const dispatch = useDispatch();
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
const {
storedTheme
} = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateResponseScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className='flex-1 mt-2'>
<div className='mb-1 title text-xs'>Pre Request</div>
<CodeEditor
collection={collection} value={requestScript || ''}
theme={storedTheme}
onEdit={onRequestScriptEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</div>
<div className='flex-1 mt-6'>
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
<CodeEditor
collection={collection} value={responseScript || ''}
theme={storedTheme}
onEdit={onResponseScriptEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</div>
</StyledWrapper>
);
};
export default Script;

View File

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

View File

@@ -1,45 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestTests({
tests: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection} value={tests || ''}
theme={storedTheme}
onEdit={onEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default Tests;

View File

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

View File

@@ -1,56 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -1,145 +0,0 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import StyledWrapper from './StyledWrapper';
const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const handleAddVar = () => {
dispatch(
addVar({
type: varType,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
_var.name = e.target.value;
break;
}
case 'value': {
_var.value = e.target.value;
break;
}
case 'enabled': {
_var.enabled = e.target.checked;
break;
}
}
dispatch(
updateVar({
type: varType,
var: _var,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveVar = (_var) => {
dispatch(
deleteVar({
type: varType,
varUid: _var.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{ varType === 'request' ? (
<td>
<div className='flex items-center'>
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var"/>
</div>
</td>
) : (
<td>
<div className='flex items-center'>
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var"/>
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
? vars.map((_var, index) => {
return (
<tr key={_var.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={_var.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, _var, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) => handleVarChange({
target: {
value: newValue
}
}, _var, 'value')}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={_var.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
+ Add
</button>
</StyledWrapper>
);
};
export default VarsTable;

View File

@@ -1,56 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
const Vars = ({ item, collection }) => {
const dispatch = useDispatch();
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
const {
storedTheme
} = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateResponseScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className='flex-1 mt-2'>
<div className='mb-1 title text-xs'>Pre Request</div>
<VarsTable item={item} collection={collection} vars={requestVars} varType='request'/>
</div>
<div className='flex-1'>
<div className='mt-1 mb-1 title text-xs'>Post Response</div>
<VarsTable item={item} collection={collection} vars={responseVars} varType='response'/>
</div>
</StyledWrapper>
);
};
export default Vars;

View File

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

View File

@@ -18,32 +18,13 @@ const StyledWrapper = styled.div`
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
border-left: solid 1px var(--color-request-dragbar-background);
}
&:hover div.drag-request-border {
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
div.graphql-docs-explorer-container {
background: white;
outline: none;
box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px;
position: absolute;
right: 0px;
z-index: 2000;
width: 350px;
height: 100%;
div.doc-explorer-title {
text-align: left;
}
div.doc-explorer-rhs {
display: flex;
border-left: solid 1px var(--color-request-dragbar-background-active);
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import toast from 'react-hot-toast';
import { useSelector, useDispatch } from 'react-redux';
import GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';
import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
@@ -11,18 +10,12 @@ import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import RunnerResults from 'components/RunnerResults';
import { DocExplorer } from '@usebruno/graphql-docs';
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
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') {
if(typeof window == 'undefined') {
return <div></div>;
}
const dispatch = useDispatch();
@@ -33,56 +26,34 @@ 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 - DEFAULT_PADDING);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : ((screenWidth - asideWidth)/2.2)); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
const [dragging, setDragging] = useState(false);
// 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;
const leftPaneWidth = (screenWidth - asideWidth)/2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
useEffect(() => {
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - 5);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if (dragging) {
if(dragging) {
e.preventDefault();
let leftPaneXPosition = e.clientX + 2;
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
return;
}
setLeftPaneWidth(leftPaneXPosition- asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
setLeftPaneWidth(e.clientX - asideWidth - 5);
setRightPaneWidth(screenWidth - (e.clientX) - 5);
}
};
const handleMouseUp = (e) => {
if (dragging) {
if(dragging) {
e.preventDefault();
setDragging(false);
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
dispatch(updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
}));
}
};
const handleDragbarMouseDown = (e) => {
@@ -90,6 +61,11 @@ const RequestTabPanel = () => {
setDragging(true);
};
let schema = null;
// let {
// schema
// } = useGraphqlSchema('https://api.spacex.land/graphql');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
@@ -100,57 +76,69 @@ const RequestTabPanel = () => {
};
}, [dragging, asideWidth]);
if (!activeTabUid) {
return <Welcome />;
if(!activeTabUid) {
return (
<Welcome/>
);
}
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
if(!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
if (!collection || !collection.uid) {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const showRunner = collection.showRunner;
if(showRunner) {
return <RunnerResults collection={collection}/>;
if(!collection || !collection.uid) {
return (
<div className="pb-4 px-4">Collection not found!</div>
);
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
}
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
if(!item || !item.uid) {
return (
<RequestNotFound itemUid={activeTabUid}/>
);
};
const handleRun = async () => dispatch(sendRequest(item, collection.uid));
const onGraphqlQueryChange = (value) => {};
const runQuery = async () => {};
return (
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
<div className="pt-4 pb-3 px-4">
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
<QueryUrl
item = {item}
collection={collection}
handleRun={handleRun}
/>
</div>
<section className="main flex flex-grow pb-4 relative">
<section className="main flex flex-grow pb-4">
<section className="request-pane">
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
<div
className="px-4"
style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}}
>
{item.type === 'graphql-request' ? (
<GraphQLRequestPane
item={item}
collection={collection}
onRunQuery={runQuery}
schema={schema}
leftPaneWidth={leftPaneWidth}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
value={item.request.body.graphql.query}
onQueryChange={onGraphqlQueryChange}
/>
) : null}
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} /> : null}
{item.type === 'http-request' ? (
<HttpRequestPane
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
/>
) : null}
</div>
</section>
@@ -159,25 +147,16 @@ const RequestTabPanel = () => {
</div>
<section className="response-pane flex-grow">
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
<ResponsePane
item={item}
collection={collection}
rightPaneWidth={rightPaneWidth}
response={item.response}
/>
</section>
</section>
{item.type === 'graphql-request' ? (
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
<DocExplorer schema={schema} ref={(r) => docExplorerRef.current = r}>
<button
className='mr-2'
onClick={toggleDocs}
aria-label="Close Documentation Explorer"
>
{'\u2715'}
</button>
</DocExplorer>
</div>
): null}
</StyledWrapper>
);
)
};
export default RequestTabPanel;
export default RequestTabPanel;

View File

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

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