Compare commits

...

75 Commits

Author SHA1 Message Date
Anoop M D
e6265db353 chore: backstage catalog 2022-12-16 19:18:16 +05:30
Vinod Godti
cffef31f97 RequestPane body form input text color visibility fix in dark mode (#70) 2022-11-10 18:29:17 +05:30
Nash
b93be5a846 Set default theme to the user's browser theme (#69) 2022-11-09 22:55:30 +05:30
Anoop M D
544765af3e fix: fixed graphql docs height overflow (#65) 2022-11-08 03:45:30 +05:30
Anoop M D
2393092248 feat: graphql docs explorer (#65) 2022-11-08 03:35:58 +05:30
Anoop M D
dcdeb78995 release: bruno-graphql-docs@0.1.0 2022-11-08 03:34:25 +05:30
Ankur Singh Chauhan
e16650d4a7 Bugfix/split line (#66)
* Issue:62,fix Split line should have max squeeze limit
* added limit to right pane also
2022-11-07 18:22:09 +05:30
Anoop M D
62ed489847 chore: bumped bruno-schema in bruno-app 2022-11-07 03:14:48 +05:30
Anoop M D
2930eb29ec chore: release bruno-schema 0.2.0 2022-11-07 03:13:22 +05:30
Anoop M D
eecb60f5cf Merge branch 'main' of github.com:usebruno/bruno into main 2022-11-07 03:06:18 +05:30
Ankur Singh Chauhan
82fb2819c2 Issue:62,fix Split line should have max squeeze limit (#64)
Co-authored-by: Ankur Singh Chauhan <anx450z@gmail.com>
2022-11-07 03:03:35 +05:30
Anoop M D
2aef7c61a4 feat: graphql support (#65) 2022-11-07 02:56:58 +05:30
Anoop M D
530af1f929 chore: fix peer deps issue for rollup 2022-11-06 14:41:10 +05:30
Anoop M D
3753fd1e20 feat: standalone graphiql docs explorer 2022-11-06 01:04:30 +05:30
Anoop M D
a59ae75809 fix: fixed postman import url issue 2022-11-05 01:00:21 +05:30
Anoop M D
4c18c27406 chore: allow legacy peer deps while while running playwright tests 2022-11-05 00:21:15 +05:30
Anoop M D
5d25fdcf7a chore: bumped rollup version 2022-11-05 00:16:54 +05:30
Anoop M D
8cfdb3ebcb Merge branch 'main' of github.com:usebruno/bruno 2022-11-04 23:42:43 +05:30
Anoop M D
9e64ea5439 feat: package init for graphql-docs 2022-11-04 23:42:34 +05:30
shash68i
23c8044973 chore: prettier script added in root bruno/package.json (#63) 2022-11-02 15:22:48 +05:30
shash68i
46ac15dd81 Collection items whole div made as action button and cursor pointer added to the items (#61) 2022-11-01 00:50:51 +05:30
depa panjie purnama
5ad9be4f6b feat: Create New Request e2e test (#52)
* add selector ID
* add createNewRequest flow
* selector update
2022-10-31 16:50:57 +05:30
shash68i
f46625c689 Added missing <br /> between Website and Discord link in readme.md (#60) 2022-10-31 16:49:27 +05:30
Anoop M D
c0e1bf6bc2 chore: added discord invite link in readme 2022-10-31 03:14:29 +05:30
Anoop M D
874ca07f39 chore: added discord link 2022-10-31 00:58:36 +05:30
Anoop M D
a291e7f345 fix: fixed mac electron build issues 2022-10-31 00:58:20 +05:30
Anoop M D
c9cabfde35 release: bumped version 2022-10-30 20:07:11 +05:30
Anoop M D
fde15d7c31 Merge branch 'main' of github.com:usebruno/bruno into main 2022-10-30 19:58:06 +05:30
Anoop M D
11defe18ca chore: disable telemetry during playwright test execution 2022-10-30 19:56:54 +05:30
anusreesubash
54b14a005d fix(#53): fix for response editor search issue (#55) 2022-10-30 17:50:38 +05:30
Anoop M D
f283df2a1b feat: posthog telemetry 2022-10-30 17:48:36 +05:30
Anoop M D
820c99711b fix: fixed missing sidebar bg color 2022-10-30 04:05:52 +05:30
Anoop M D
df1cd4aff9 Merge branch 'feature/import-postman-collection' 2022-10-30 02:01:22 +05:30
Anoop M D
481486cd1c feat: import postman collection (#45) 2022-10-30 02:00:54 +05:30
Anoop M D
bf4c26de33 feat: refactored import collection 2022-10-30 00:09:24 +05:30
depa panjie purnama
c3fa473dae use fakerjs as random test data (#51) 2022-10-27 23:23:04 +05:30
depa panjie purnama
90a29918d0 feat: Create Collection e2e test (#50)
* add selector IDs
* add Create Collection e2e test
2022-10-26 21:59:37 +05:30
Anoop M D
c0698adcb3 chore: cleanup 2022-10-25 14:58:57 +05:30
Anoop M D
0d0f99e810 chore: cleanup 2022-10-25 14:57:53 +05:30
Anoop M D
7f5a6d5566 chore: using npm i instead of ci in playwright github actions 2022-10-25 00:52:42 +05:30
depa panjie purnama
dc68d511bd add e2e test using playwright (#44) 2022-10-25 00:42:53 +05:30
Anoop M D
0fceaf6918 chore: updated readme 2022-10-23 22:40:26 +05:30
Anoop M D
831223711a chore: updated app description 2022-10-23 20:57:35 +05:30
Anoop M D
e4cf3750bd chore: fixed missing screenshote in readme 2022-10-23 20:51:12 +05:30
Anoop M D
01e15b7fc1 Merge branch 'main' of github.com:usebruno/bruno 2022-10-23 19:48:33 +05:30
Anoop M D
3bf3d30ce8 fix: environment var interpolation issues 2022-10-23 19:48:02 +05:30
Vijay Hudge
bf6e6b29f5 Updated Readme with all contributors shown as image (#43) 2022-10-23 17:17:45 +05:30
Sean
075aaaebec Fixed spelling error in contributing.md and updated pull request section (#41) 2022-10-23 13:50:42 +05:30
Anoop M D
1136f1b105 release: v0.2.0 2022-10-23 13:43:12 +05:30
Anoop M D
5c8e66b684 feat: using a lighter shade of blue for dark theme 2022-10-23 12:53:24 +05:30
Anoop M D
09faf46635 chore: updated bruno tagline 2022-10-23 12:39:19 +05:30
Anoop M D
ef28637d0c Merge branch 'feature/dark-mode' 2022-10-23 12:37:20 +05:30
Anoop M D
51784d08cd fix: fixed bugs 2022-10-23 11:57:35 +05:30
Anoop M D
96f50b0c6d feat: dark-mode completed :) 2022-10-23 11:26:16 +05:30
Anoop M D
2ba6e4823d feat: dark-mode (response status and support modal) 2022-10-23 05:32:47 +05:30
Anoop M D
04a0a37ca4 feat: dark-mode (code editor) 2022-10-23 04:48:43 +05:30
Anoop M D
23400a77f8 feat: dark mode (tabs and request tabs) 2022-10-23 03:21:23 +05:30
Anoop M D
4718c77e3d feat: dark model (modals) 2022-10-23 02:14:59 +05:30
anusreesubash
0cde789697 fix: fixed issue renaming workspaces and creating collections (#40) 2022-10-22 16:32:42 +05:30
Anoop M D
6be2818bfb feat: dark mode (environment selector, query url) 2022-10-22 16:00:04 +05:30
Anoop M D
ac4e3a9f3d chore: rollback temporary fix to make the vercel build pass 2022-10-22 00:47:24 +05:30
Anoop M D
0ecaba27a6 fix: temporary fix for failing vercel build since its not finding nextjs in root 2022-10-22 00:37:55 +05:30
Anoop M D
2c8ef7b626 chore: bumped nextjs to 12.3.1 2022-10-22 00:34:01 +05:30
Bram Hoven
ea3a9394c9 Improve error for workspace deletion (#39)
* Move dmg-license to optionalDependencies so it can be installed on windows
* Extend toastError to accept a default error message
* Throw BrunoError when in deleteWorkspace
* Handle errors with the toastError
* Use existing parseError for getting errorMsg in toastError
2022-10-21 23:57:42 +05:30
Anoop M D
995c6b3fd0 chore: updated landing image 2022-10-21 18:46:23 +05:30
Anoop M D
bd6ce6a67b feat: added contributing guide 2022-10-21 18:41:28 +05:30
Anoop M D
8e70e191e1 feat: darkmode (sidebar, menubar and welcome page #23) 2022-10-21 04:20:23 +05:30
Sean
cbdfabb4db Features/dark mode (#37)
* Added use local storage hook
* Added theme
* Added theme support
* Added theme provider
* Added dark theme for sidebar
* Added dark theme for main content area
* Added theme
* Added theme support
* Added theme provider
* Added dark theme for sidebar
* Added dark theme for main content area
2022-10-21 00:44:09 +05:30
Sean
3c3c9a6026 Added use local storage hook (#36) 2022-10-20 16:30:35 +05:30
Anoop M D
6b68857b81 chore: added package-lock.json to tauri and testbench packages 2022-10-20 15:18:09 +05:30
Anoop M D
405f253eef chore: deleted package-lock.json in tauri and testbench packages 2022-10-20 15:17:16 +05:30
Anoop M D
4b6439785f chore: added package-lock.json to gitignore 2022-10-20 15:16:33 +05:30
Anoop M D
f9806e69a5 Merge branch 'main' of github.com:usebruno/bruno 2022-10-20 15:11:15 +05:30
Anoop M D
ba219d66db feat: prettier config 2022-10-20 15:09:30 +05:30
Sean
a41f4fe024 fix: dependency error when contributing (#34) 2022-10-19 19:34:42 +05:30
246 changed files with 6967 additions and 26089 deletions

27
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

7
.gitignore vendored
View File

@@ -2,6 +2,9 @@
# dependencies
node_modules
yarn.lock
pnpm-lock.yaml
package-lock.json
.pnp
.pnp.js
@@ -14,6 +17,7 @@ chrome-extension
chrome-extension.pem
chrome-extension.crx
bruno.zip
*.zip
# misc
.DS_Store
@@ -34,3 +38,6 @@ yarn-error.log*
/renderer
/renderer/.next/
/renderer/out/
/test-results/
/playwright-report/
/playwright/.cache/

2
.nvmrc
View File

@@ -1 +1 @@
v14.17.0
v14.18.0

BIN
assets/images/landing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

16
catalog-info.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: "bruno"
links:
- url: https://example.com/user
title: Examples Users
icon: user
- url: https://example.com/group
title: Example Group
icon: group
spec:
type: component
lifecycle: production
owner: anoop
system: tech-docs

53
contributing.md Normal file
View File

@@ -0,0 +1,53 @@
## 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
```bash
# clone and cd into bruno
# use Node 14.x, Npm 8.x
# Install deps (note that we use npm workspaces)
npm i
# run next app
npm run dev:web
# run electron app
# neededonly if you want to test changes related to electron app
# please note that both web and electron use the same code
# if it works in web, then it should also work in electron
npm run dev:electron
# open in browser
open http://localhost:3000
```
### Raising Pull Request
- Please keep the PR's small and focused on one thing
- 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,4 +1,5 @@
## development
```bash
# install deps
npm i
@@ -13,9 +14,14 @@ npm run dev --workspace=packages/bruno-electron
npm run build --workspace=packages/bruno-app
```
## fix
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
# testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
```
```

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
const axios = require('axios');
const FormData = require('form-data');
const { ipcMain } = require('electron');
const { forOwn, extend } = require('lodash');
const registerIpc = () => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, request) => {
try {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
});
extend(request.headers, form.getHeaders());
request.data = form;
}
const result = await axios(request);
return {
status: result.status,
headers: result.headers,
data: result.data
};
} catch (error) {
if(error.response) {
return {
status: error.response.status,
headers: error.response.headers,
data: error.response.data
};
}
return {
status: -1,
headers: [],
data: null
};
}
});
};
module.exports = registerIpc;

View File

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

View File

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

20737
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,17 +6,27 @@
"packages/bruno-electron",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-testbench"
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@playwright/test": "^1.27.1",
"jest": "^29.2.0",
"randomstring": "^1.2.2"
},
"scripts": {
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
"build:electron": "./scripts/build-electron.sh"
"build:electron": "./scripts/build-electron.sh",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report"
},
"overrides": {
"rollup": "3.2.5"
}
}

View File

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

View File

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

View File

@@ -6,6 +6,8 @@
"paths": {
"assets/*": ["src/assets/*"],
"components/*": ["src/components/*"],
"hooks/*": ["src/hooks/*"],
"themes/*": ["src/themes/*"],
"api/*": ["src/api/*"],
"pageComponents/*": ["src/pageComponents/*"],
"providers/*": ["src/providers/*"],

View File

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

View File

@@ -5,16 +5,18 @@
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@usebruno/schema": "0.1.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/schema": "0.2.0",
"@usebruno/graphql-docs": "0.1.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -29,16 +31,20 @@
"idb": "^7.0.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "^12.1.0",
"next": "12.3.1",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.4.0",
"react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"reckonjs": "^0.1.2",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
@@ -57,6 +63,7 @@
"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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,25 @@
import axios from "axios";
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'
};
apiClient.interceptors.request.use(
(config) => {
const headers = {
'Content-Type': 'application/json'
};
return ({
...config,
headers: headers
});
}, error => Promise.reject(error));
return {
...config,
headers: headers
};
},
(error) => Promise.reject(error)
);
apiClient.interceptors.response.use((response) =>
response,
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error.response ? error.response.data : error);
}

View File

@@ -1,30 +1,94 @@
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: var(--color-text);
color: ${(props) => props.theme.text};
.collection-options {
svg {
position: relative;

View File

@@ -1,40 +1,41 @@
import React from "react";
import Modal from "components/Modal/index";
import {IconSpeakerphone, IconBrandTwitter} from "@tabler/icons";
import StyledWrapper from "./StyledWrapper";
import GithubSvg from 'assets/github.svg';
import React from 'react';
import Modal from 'components/Modal/index';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } 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://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 href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<img src={GithubSvg.src} style={{width: "18px"}}/>
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">Discord</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
</a>
</div>
<div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-center">
<IconBrandTwitter size={18} strokeWidth={2}/><span className="label ml-2">Twitter</span>
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span>
</a>
</div>
</div>
</Modal>
</StyledWrapper>
);
}
};
export default BrunoSupport;

View File

@@ -2,13 +2,31 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.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;
}
`;
export default StyledWrapper;

View File

@@ -37,33 +37,36 @@ export default class QueryEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
readOnly: this.props.readOnly ? 'nocursor' : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
readOnly: this.props.readOnly,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if(this.props.onRun) {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if(this.props.onSave) {
if (this.props.onSave) {
this.props.onSave();
}
},
'Tab': function(cm){
cm.replaceSelection(" " , "end");
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
}
},
}
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -82,14 +85,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);
this.editor.setOption("mode", this.props.mode);
this.editor.setOption('mode', this.props.mode);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
this.ignoreChangeEvent = false;
}
@@ -106,7 +109,7 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full"
aria-label="Code Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>

View File

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

View File

@@ -2,19 +2,10 @@ 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: #efefef;
background-color: ${(props) => props.theme.sidebar.workspace.bg};
border-radius: 15px;
.caret {

View File

@@ -3,68 +3,77 @@ import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
import EnvironmentSettings from "../EnvironmentSettings";
import EnvironmentSettings from '../EnvironmentSettings';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const EnvironmentSelector = ({collection}) => {
const EnvironmentSelector = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const { environments, activeEnvironmentUid } = collection;
const activeEnvironment = activeEnvironmentUid ? find(environments, e => e.uid === activeEnvironmentUid) : null;
const 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}/>
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);
});
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const onSelect = (environment) => {
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
.then(() => {
if(environment) {
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"));
.catch((err) => console.log(err) && toast.error('An error occured while selecting the environment'));
};
return (
<StyledWrapper>
<div className="flex items-center cursor-pointer environment-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-end'>
{(environments && environments.length) ? environments.map((e) => (
<div className="dropdown-item" key={e.uid} onClick={() => {
onSelect(e);
<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();
}}>
<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);
}}>
onSelect(null);
}}
>
<span>No Environment</span>
</div>
<div className="dropdown-item" style={{borderTop: 'solid 1px #e7e7e7'}} onClick={() => setOpenSettingsModal(true)}>
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5}/>
<IconSettings size={18} strokeWidth={1.5} />
</div>
<span>Settings</span>
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)}/>}
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)} />}
</StyledWrapper>
);
};

View File

@@ -1,75 +1,70 @@
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,39 +1,31 @@
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

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};;
font-size: 0.8125rem;
user-select: none;
}
@@ -24,18 +25,19 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
}
input[type="text"] {
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: transparent;
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -7,23 +7,20 @@ import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions
import reducer from './reducer';
import StyledWrapper from './StyledWrapper';
const EnvironmentVariables = ({environment, collection}) => {
const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch();
const [state, reducerDispatch] = useReducer(reducer, {hasChanges: false, variables: environment.variables || []});
const {
variables,
hasChanges
} = state;
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");
toast.success('Changes saved successfully');
reducerDispatch({
type: 'CHANGES_SAVED'
});
})
.catch(() => toast.error("An error occured while saving the changes"));
.catch(() => toast.error('An error occured while saving the changes'));
};
const addVariable = () => {
@@ -34,16 +31,16 @@ const EnvironmentVariables = ({environment, collection}) => {
const handleVarChange = (e, _variable, type) => {
const variable = cloneDeep(_variable);
switch(type) {
case 'name' : {
switch (type) {
case 'name': {
variable.name = e.target.value;
break;
}
case 'value' : {
case 'value': {
variable.value = e.target.value;
break;
}
case 'enabled' : {
case 'enabled': {
variable.enabled = e.target.checked;
break;
}
@@ -53,14 +50,14 @@ const EnvironmentVariables = ({environment, collection}) => {
variable
});
};
const handleRemoveVars = (variable) => {
reducerDispatch({
type: 'DELETE_VAR',
variable
});
};
return (
<StyledWrapper className="w-full mt-6 mb-6">
<table>
@@ -72,48 +69,53 @@ const EnvironmentVariables = ({environment, collection}) => {
</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}
{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>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
+ Add Variable
</button>
</div>
<div>
@@ -122,6 +124,6 @@ const EnvironmentVariables = ({environment, collection}) => {
</button>
</div>
</StyledWrapper>
)
);
};
export default EnvironmentVariables;
export default EnvironmentVariables;

View File

@@ -20,7 +20,7 @@ const reducer = (state, action) => {
case 'UPDATE_VAR': {
return produce(state, (draft) => {
const variable = find(draft.variables, (v) => v.uid === action.variable.uid);
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;
@@ -47,4 +47,4 @@ const reducer = (state, action) => {
}
};
export default reducer;
export default reducer;

View File

@@ -1,34 +1,33 @@
import React, {useState } from "react";
import { IconEdit, IconTrash, IconDatabase } from "@tabler/icons";
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 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);
console.log(environment);
return (
<div className="px-6 flex-grow flex flex-col pt-6" style={{maxWidth: '700px'}}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
<div className="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}/>
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
<span className="ml-1 font-semibold">{environment.name}</span>
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
<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}/>
<EnvironmentVariables key={environment.uid} environment={environment} collection={collection} />
</div>
</div>
);
};

View File

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

View File

@@ -1,42 +1,40 @@
import React, { useEffect, useState, forwardRef, useRef } from "react";
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);
useEffect(() => {
setSelectedEnvironment(environments[0]);
}, []);
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
}, [environments]);
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,75 +1,70 @@
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,50 +1,34 @@
import Modal from "components/Modal/index";
import React, { useState } from "react";
import CreateEnvironment from "./CreateEnvironment";
import EnvironmentList from "./EnvironmentList";
import StyledWrapper from "./StyledWrapper";
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

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

View File

@@ -1,11 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div`
&.modal--animate-out{
animation: fade-out 0.5s forwards cubic-bezier(.19,1,.22,1);
&.modal--animate-out {
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
.bruno-modal-card {
animation: fade-and-slide-out-from-top .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-out-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
}
@@ -23,8 +23,8 @@ const Wrapper = styled.div`
}
.bruno-modal-card {
animation-duration: .85s;
animation-delay: .1s;
animation-duration: 0.85s;
animation-delay: 0.1s;
background: var(--color-background-top);
border-radius: var(--border-radius);
position: relative;
@@ -33,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 .50s forwards cubic-bezier(.19,1,.22,1);
animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-header {
@@ -66,8 +66,8 @@ const Wrapper = styled.div`
justify-content: space-between;
align-items: center;
text-transform: uppercase;
color: rgb(86 86 86);
background-color: #f1f1f1;
color: ${(props) => props.theme.modal.title.color};
background-color: ${(props) => props.theme.modal.title.bg};
font-size: 0.75rem;
padding: 12px;
font-weight: 600;
@@ -77,7 +77,7 @@ const Wrapper = styled.div`
.close {
font-size: 1.3rem;
line-height: 1;
color: #000;
color: ${(props) => props.theme.modal.iconColor};
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
margin-top: -2px;
@@ -90,7 +90,30 @@ const Wrapper = styled.div`
.bruno-modal-content {
flex-grow: 1;
background-color: #fff;
background-color: ${(props) => props.theme.modal.body.bg};
.textbox {
line-height: 1.42857143;
border: 1px solid #ccc;
padding: 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out .1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
.bruno-form {
color: ${(props) => props.theme.modal.body.color};
}
}
.bruno-modal-backdrop {
@@ -102,25 +125,32 @@ const Wrapper = styled.div`
will-change: opacity;
background: transparent;
&:before{
content: "";
&:before {
content: '';
height: 100%;
width: 100%;
left: 0;
opacity: .4;
opacity: ${(props) => props.theme.modal.backdrop.opacity};
top: 0;
background: black;
position: fixed;
}
animation: fade-in .1s forwards cubic-bezier(.19,1,.22,1);
animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
}
.bruno-modal-footer {
background-color: white;
background-color: ${(props) => props.theme.modal.body.bg};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&.modal-footer-none {
.bruno-modal-content {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
`;
export default Wrapper;

View File

@@ -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,13 +12,9 @@ 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';
@@ -28,32 +24,21 @@ const ModalFooter = ({confirmText, cancelText, handleSubmit, handleCancel, confi
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;
@@ -72,23 +57,26 @@ const Modal = ({
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,7 +14,6 @@ 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,27 +25,36 @@ const Navbar = () => {
{/* <FontAwesomeIcon className="ml-2" icon={faCaretDown} style={{fontSize: 13}}/> */}
</div>
<div className="collection-dropdown flex flex-grow items-center justify-end">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement='bottom-start'>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}>
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setModalOpen(true);
}}
>
Create Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}
>
Import Collection
</div>
<div className="dropdown-item" onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
}}
>
Settings
</div>
</Dropdown>
</div>
</StyledWrapper>
)
);
};
export default Navbar;

View File

@@ -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

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
@@ -24,18 +25,19 @@ 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};
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -6,52 +6,58 @@ import { useDispatch } from 'react-redux';
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const FormUrlEncodedParams = ({item, collection}) => {
const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
const addParam = () => {
dispatch(addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
dispatch(
addFormUrlEncodedParam({
itemUid: item.uid,
collectionUid: 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 'description' : {
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled' : {
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>
@@ -64,56 +70,64 @@ const FormUrlEncodedParams = ({item, collection}) => {
</tr>
</thead>
<tbody>
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
{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,55 +1,30 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.react-tabs__tab-list {
border-bottom: none !important;
padding-top: 0;
padding-left: 0 !important;
display: flex;
align-items: center;
margin: 0;
.react-tabs__tab {
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
user-select: none;
border-bottom: solid 2px transparent;
margin-right: 20px;
color: rgb(125 125 125);
outline: none !important;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&:after {
display: none !important;
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
.react-tabs__tab--selected {
border: none;
color: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
border: none;
outline: none !important;
box-shadow: none !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
border-color: var(--color-tab-active-border) !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

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

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const fetchSchema = (endpoint) => {
const introspectionQuery = getIntrospectionQuery();
const queryParams = {
query: introspectionQuery
};
return fetch(endpoint, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(queryParams)
});
}
const useGraphqlSchema = (endpoint) => {
const 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);
fetchSchema(endpoint)
.then((res) => res.json())
.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

@@ -10,17 +10,21 @@ 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: #322e2c !important;
border-bottom: solid 2px var(--color-tab-active-border) !important;
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

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

View File

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
@@ -24,18 +25,19 @@ 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};
&:focus{
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;

View File

@@ -6,52 +6,58 @@ import { useDispatch } from 'react-redux';
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const MultipartFormParams = ({item, collection}) => {
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
const addParam = () => {
dispatch(addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
dispatch(
addMultipartFormParam({
itemUid: item.uid,
collectionUid: 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 'description' : {
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled' : {
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>
@@ -64,56 +70,64 @@ const MultipartFormParams = ({item, collection}) => {
</tr>
</thead>
<tbody>
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
{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,15 +2,33 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px var(--color-codemirror-border);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
/* todo: find a better way */
height: calc(100vh - 250px);
height: calc(100vh - 220px);
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.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;
}
`;
export default StyledWrapper;

View File

@@ -38,99 +38,90 @@ export default class QueryEditor extends React.Component {
tabSize: 2,
mode: 'graphql',
theme: this.props.editorTheme || 'graphiql',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
readOnly: this.props.readOnly ? 'nocursor' : false,
foldGutter: {
minFoldSize: 4,
minFoldSize: 4
},
lint: {
schema: this.props.schema,
validationRules: this.props.validationRules ?? null,
// linting accepts string or FragmentDefinitionNode[]
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
hintOptions: {
schema: this.props.schema,
closeOnUnfocus: false,
completeSingle: false,
container: this._node,
externalFragments: this.props?.externalFragments,
externalFragments: this.props?.externalFragments
},
info: {
schema: this.props.schema,
renderDescription: (text) => md.render(text),
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference),
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
jump: {
schema: this.props.schema,
onClick: (reference) =>
this.props.onClickReference && this.props.onClickReference(reference)
onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)
},
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: {
'Cmd-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () =>
editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Ctrl-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
'Cmd-Enter': () => {
if (this.props.onRunQuery) {
this.props.onRunQuery();
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onRunQuery) {
this.props.onRunQuery();
if (this.props.onRun) {
this.props.onRun();
}
},
'Shift-Ctrl-C': () => {
if (this.props.onCopyQuery) {
this.props.onCopyQuery();
}
},
'Shift-Ctrl-P': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();
}
},
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
'Shift-Ctrl-F': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();
}
},
'Shift-Ctrl-M': () => {
if (this.props.onMergeQuery) {
this.props.onMergeQuery();
}
},
'Cmd-S': () => {
if (this.props.onRunQuery) {
// empty
if (this.props.onSave) {
this.props.onSave();
return false;
}
},
'Ctrl-S': () => {
if (this.props.onRunQuery) {
// empty
if (this.props.onSave) {
this.props.onSave();
return false;
}
},
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent'
}
}));
if (editor) {
editor.on('change', this._onEdit);
@@ -152,14 +143,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');
}
this.ignoreChangeEvent = false;
}
@@ -175,17 +166,20 @@ export default class QueryEditor extends React.Component {
render() {
return (
<StyledWrapper
className="h-full"
className="h-full w-full"
aria-label="Query Editor"
ref={node => {
ref={(node) => {
this._node = node;
}}
/>
);
}
_onKeyUp = (_cm, event) => {
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
_onKeyUp = (_cm, e) => {
if (e.metaKey || e.ctrlKey || e.altKey) {
return;
}
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
this.editor.execCommand('autocomplete');
}
};

View File

@@ -7,10 +7,7 @@
import escapeHTML from 'escape-html';
import MD from 'markdown-it';
import {
GraphQLNonNull,
GraphQLList
} from 'graphql';
import { GraphQLNonNull, GraphQLList } from 'graphql';
const md = new MD();
@@ -18,85 +15,65 @@ 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

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};;
font-size: 0.8125rem;
user-select: none;
}
@@ -27,18 +28,19 @@ 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

@@ -7,52 +7,58 @@ import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/Red
import StyledWrapper from './StyledWrapper';
const QueryParams = ({item, collection}) => {
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const handleAddParam = () => {
dispatch(addQueryParam({
itemUid: item.uid,
collectionUid: collection.uid,
}));
dispatch(
addQueryParam({
itemUid: item.uid,
collectionUid: 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 'description' : {
case 'description': {
param.description = e.target.value;
break;
}
case 'enabled' : {
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 (
@@ -67,58 +73,64 @@ const QueryParams = ({item, collection}) => {
</tr>
</thead>
<tbody>
{params && params.length ? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.value}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={param.description}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
{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: .25rem .6rem !important;
padding: 0.25rem 0.6rem !important;
}
}

View File

@@ -3,43 +3,48 @@ import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const HttpMethodSelector = ({method, onMethodSelect}) => {
const HttpMethodSelector = ({ method, onMethodSelect }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
<div className="flex-grow font-medium">{method}</div>
<div><IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2}/></div>
<div 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>
);
});
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: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
div.input-container {
background-color: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
input {
background-color: var(--color-sidebar-background);
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
outline: none;
box-shadow: none;

View File

@@ -3,48 +3,58 @@ import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import StyledWrapper from './StyledWrapper';
import SendSvg from 'assets/send.svg';
const QueryUrl = ({item, collection, handleRun}) => {
const QueryUrl = ({ item, collection, handleRun }) => {
const { theme } = useTheme();
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const onUrlChange = (value) => {
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
dispatch(
requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
})
);
};
const onMethodSelect = (verb) => {
dispatch(updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
}));
dispatch(
updateRequestMethod({
method: verb,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="flex items-center">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect}/>
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full mousetrap"
type="text" value={url}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
type="text"
value={url}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={(event) => onUrlChange(event.target.value)}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" onClick={handleRun}>
<img src={SendSvg.src} style={{width: '22px'}}/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
</div>
</div>
</StyledWrapper>
)
);
};
export default QueryUrl;

View File

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

View File

@@ -7,76 +7,89 @@ 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');
}}>
<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

@@ -4,27 +4,33 @@ 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',
@@ -37,31 +43,21 @@ const RequestBody = ({item, collection}) => {
xml: body.xml
};
return(
return (
<StyledWrapper className="w-full">
<CodeEditor
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}
onSave={onSave}
mode={codeMirrorMode[bodyMode]}
/>
<CodeEditor theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
</StyledWrapper>
);
}
if(bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection}/>;
if (bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection} />;
}
if(bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection}/>;
if (bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection} />;
}
return(
<StyledWrapper className="w-full">
No Body
</StyledWrapper>
);
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;

View File

@@ -6,12 +6,13 @@ const Wrapper = styled.div`
border-collapse: collapse;
font-weight: 600;
thead, td {
border: 1px solid #efefef;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: #616161;
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
@@ -22,22 +23,21 @@ const Wrapper = styled.div`
.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

@@ -6,52 +6,58 @@ import { useDispatch } from 'react-redux';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const RequestHeaders = ({item, collection}) => {
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const addHeader = () => {
dispatch(addRequestHeader({
itemUid: item.uid,
collectionUid: collection.uid,
}));
dispatch(
addRequestHeader({
itemUid: item.uid,
collectionUid: 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 'description' : {
case 'description': {
header.description = e.target.value;
break;
}
case 'enabled' : {
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>
@@ -64,56 +70,64 @@ const RequestHeaders = ({item, collection}) => {
</tr>
</thead>
<tbody>
{headers && headers.length ? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={header.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20}/>
</button>
</div>
</td>
</tr>
);
}) : null}
{headers && headers.length
? headers.map((header, index) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.value}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={header.description}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
/>
</td>
<td>
<div className="flex items-center">
<input type="checkbox" checked={header.enabled} className="mr-3 mousetrap" onChange={(e) => handleHeaderValueChange(e, header, 'enabled')} />
<button onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-header select-none" onClick={addHeader}>+ Add Header</button>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
</StyledWrapper>
)
);
};
export default RequestHeaders;
export default RequestHeaders;

View File

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

@@ -2,22 +2,22 @@ import React from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const RequestNotFound = ({itemUid}) => {
const RequestNotFound = ({ itemUid }) => {
const dispatch = useDispatch();
const closeTab = () => {
dispatch(closeTabs({
tabUids: [itemUid]
}));
dispatch(
closeTabs({
tabUids: [itemUid]
})
);
};
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
<div>Request no longer exists.</div>
<div className="mt-2">
This can happen when the yml file associated with this request was deleted on your filesystem.
</div>
<div className="mt-2">This can happen when the yml file associated with this request was deleted on your filesystem.</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import find from 'lodash/find';
import toast from 'react-hot-toast';
import { useSelector, useDispatch } from 'react-redux';
@@ -12,12 +12,16 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
const DEFAULT_PADDING = 5;
const RequestTabPanel = () => {
if(typeof window == 'undefined') {
if (typeof window == 'undefined') {
return <div></div>;
}
const dispatch = useDispatch();
@@ -28,34 +32,56 @@ const RequestTabPanel = () => {
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : ((screenWidth - asideWidth)/2.2)); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
const [dragging, setDragging] = useState(false);
// 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 - 5);
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if(dragging) {
if (dragging) {
e.preventDefault();
setLeftPaneWidth(e.clientX - asideWidth - 5);
setRightPaneWidth(screenWidth - (e.clientX) - 5);
let leftPaneXPosition = e.clientX + 2;
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
return;
}
setLeftPaneWidth(leftPaneXPosition- asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
}
};
const handleMouseUp = (e) => {
if(dragging) {
if (dragging) {
e.preventDefault();
setDragging(false);
dispatch(updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - 5
}));
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
}
};
const handleDragbarMouseDown = (e) => {
@@ -63,11 +89,6 @@ const RequestTabPanel = () => {
setDragging(true);
};
let schema = null;
// let {
// schema
// } = useGraphqlSchema('https://api.spacex.land/graphql');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
@@ -78,74 +99,52 @@ const RequestTabPanel = () => {
};
}, [dragging, asideWidth]);
if(!activeTabUid) {
return (
<Welcome/>
);
if (!activeTabUid) {
return <Welcome />;
}
if(!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
if(!collection || !collection.uid) {
return (
<div className="pb-4 px-4">Collection not found!</div>
);
if (!collection || !collection.uid) {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const item = findItemInCollection(collection, activeTabUid);
if(!item || !item.uid) {
return (
<RequestNotFound itemUid={activeTabUid}/>
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
}
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
);
};
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid))
.catch((err) => toast.custom((t) => (<NetworkError onClose={() => toast.dismiss(t.id)}/>), {
duration: 5000
}));
};
const onGraphqlQueryChange = (value) => {};
const runQuery = async () => {};
return (
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
<div className="pt-4 pb-3 px-4">
<QueryUrl
item = {item}
collection={collection}
handleRun={handleRun}
/>
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
</div>
<section className="main flex flex-grow pb-4">
<section className="main flex flex-grow pb-4 relative">
<section className="request-pane">
<div
className="px-4"
style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}}
>
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
{item.type === 'graphql-request' ? (
<GraphQLRequestPane
onRunQuery={runQuery}
schema={schema}
leftPaneWidth={leftPaneWidth}
value={item.request.body.graphql.query}
onQueryChange={onGraphqlQueryChange}
/>
) : null}
{item.type === 'http-request' ? (
<HttpRequestPane
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
/>
) : null}
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} /> : null}
</div>
</section>
@@ -154,16 +153,25 @@ 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,6 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
`;
const StyledWrapper = styled.div``;
export default StyledWrapper;
export default StyledWrapper;

View File

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

View File

@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: nowrap;
}
.close-icon-container {
@@ -19,22 +19,22 @@ const StyledWrapper = styled.div`
.close-icon {
display: none;
color: #9f9f9f;
color: ${(props) => props.theme.requestTabs.icon.color};
width: 8px;
padding-bottom: 6px;
padding-top: 6px;
}
&:hover, &:hover .close-icon {
background-color: #eaeaea;
color: rgb(76 76 76);
&:hover,
&:hover .close-icon {
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
}
.has-changes-icon {
.has-changes-icon {
height: 24px;
}
}
`;
export default StyledWrapper;

View File

@@ -6,21 +6,23 @@ import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { IconAlertTriangle } from '@tabler/icons';
const RequestTab = ({tab, collection}) => {
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
dispatch(closeTabs({
tabUids: [tab.uid]
}))
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
};
const getMethodColor = (method = '') => {
let color = '';
method = method.toLocaleLowerCase();
switch(method) {
switch (method) {
case 'get': {
color = 'var(--color-method-get)';
break;
@@ -56,16 +58,19 @@ const RequestTab = ({tab, collection}) => {
const item = findItemInCollection(collection, tab.uid);
if(!item) {
if (!item) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-center tab-label pl-2">
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600"/>
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Not Found</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false"xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path fill="currentColor" d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
</div>
</StyledWrapper>
@@ -77,19 +82,26 @@ const RequestTab = ({tab, collection}) => {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div className="flex items-baseline tab-label pl-2">
<span className="tab-method uppercase" style={{color: getMethodColor(method), fontSize: 12}}>{method}</span>
<span className="text-gray-700 ml-1 tab-name" title={item.name}>{item.name}</span>
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
{!item.draft ? (
<svg focusable="false"xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path fill="currentColor" d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path
fill="currentColor"
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
></path>
</svg>
) : (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="16" fill="#cc7b1b" className="has-changes-icon" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3"/>
<circle cx="4" cy="4" r="3" />
</svg>
) }
)}
</div>
</StyledWrapper>
);

View File

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

View File

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

View File

@@ -1,30 +1,27 @@
import React from 'react';
const NetworkError = ({onClose}) => {
const NetworkError = ({ onClose }) => {
return (
<div className="max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex bg-red-100">
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-red-800">
Network Error
</p>
<p className="mt-2 text-xs text-gray-500">
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS.
If not, please use the chrome extension or the desktop app
</p>
</div>
<div className="max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex bg-red-100">
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-red-800">Network Error</p>
<p className="mt-2 text-xs text-gray-500">
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS. If not, please use the chrome extension or the desktop app
</p>
</div>
</div>
<div className="flex">
<button
onClick={onClose}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium focus:outline-none"
>
Close
</button>
</div>
</div>
<div className="flex">
<button
onClick={onClose}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium focus:outline-none"
>
Close
</button>
</div>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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