mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
31 Commits
feature/da
...
bugfix/res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14645d1d5f | ||
|
|
820c99711b | ||
|
|
df1cd4aff9 | ||
|
|
481486cd1c | ||
|
|
bf4c26de33 | ||
|
|
c3fa473dae | ||
|
|
90a29918d0 | ||
|
|
c0698adcb3 | ||
|
|
0d0f99e810 | ||
|
|
7f5a6d5566 | ||
|
|
dc68d511bd | ||
|
|
0fceaf6918 | ||
|
|
831223711a | ||
|
|
e4cf3750bd | ||
|
|
01e15b7fc1 | ||
|
|
3bf3d30ce8 | ||
|
|
bf6e6b29f5 | ||
|
|
075aaaebec | ||
|
|
1136f1b105 | ||
|
|
5c8e66b684 | ||
|
|
09faf46635 | ||
|
|
ef28637d0c | ||
|
|
51784d08cd | ||
|
|
96f50b0c6d | ||
|
|
0cde789697 | ||
|
|
ac4e3a9f3d | ||
|
|
0ecaba27a6 | ||
|
|
2c8ef7b626 | ||
|
|
ea3a9394c9 | ||
|
|
995c6b3fd0 | ||
|
|
bd6ce6a67b |
27
.github/workflows/playwright.yml
vendored
Normal file
27
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
- 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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,6 +17,7 @@ chrome-extension
|
||||
chrome-extension.pem
|
||||
chrome-extension.crx
|
||||
bruno.zip
|
||||
*.zip
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -37,3 +38,6 @@ yarn-error.log*
|
||||
/renderer
|
||||
/renderer/.next/
|
||||
/renderer/out/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
BIN
assets/images/landing.png
Normal file
BIN
assets/images/landing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 436 KiB |
BIN
assets/images/local-collections.png
Normal file
BIN
assets/images/local-collections.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
53
contributing.md
Normal file
53
contributing.md
Normal 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
|
||||
@@ -1,55 +0,0 @@
|
||||
const { ipcMain } = require('electron');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'Collection',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Collection',
|
||||
click () {
|
||||
ipcMain.emit('main:open-collection');
|
||||
}
|
||||
},
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo'},
|
||||
{ role: 'redo'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'cut'},
|
||||
{ role: 'copy'},
|
||||
{ role: 'paste'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload'},
|
||||
{ role: 'toggledevtools'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'resetzoom'},
|
||||
{ role: 'zoomin'},
|
||||
{ role: 'zoomout'},
|
||||
{ role: 'separator'},
|
||||
{ role: 'togglefullscreen'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{ role: 'minimize'},
|
||||
{ role: 'close'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{ label: 'Learn More'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = template;
|
||||
@@ -1,54 +0,0 @@
|
||||
const path = require('path');
|
||||
const { format } = require('url');
|
||||
const { BrowserWindow, app, Menu } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
const registerIpc = require('./ipc');
|
||||
const isDev = require('electron-is-dev');
|
||||
const prepareNext = require('electron-next');
|
||||
|
||||
setContentSecurityPolicy(`
|
||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src * 'unsafe-inline';
|
||||
base-uri 'none';
|
||||
form-action 'none';
|
||||
frame-ancestors 'none';
|
||||
`);
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
let mainWindow;
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
await prepareNext('./renderer');
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 768,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js")
|
||||
},
|
||||
});
|
||||
|
||||
const url = isDev
|
||||
? 'http://localhost:8000'
|
||||
: format({
|
||||
pathname: path.join(__dirname, '../renderer/out/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
});
|
||||
|
||||
mainWindow.loadURL(url);
|
||||
|
||||
// register all ipc handlers
|
||||
registerIpc(mainWindow);
|
||||
});
|
||||
|
||||
// Quit the app once all windows are closed
|
||||
app.on('window-all-closed', app.quit);
|
||||
47
main/ipc.js
47
main/ipc.js
@@ -1,47 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend } = require('lodash');
|
||||
|
||||
|
||||
const registerIpc = () => {
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, request) => {
|
||||
try {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
const form = new FormData();
|
||||
forOwn(request.data, (value, key) => {
|
||||
form.append(key, value);
|
||||
});
|
||||
extend(request.headers, form.getHeaders());
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
const result = await axios(request);
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
headers: result.headers,
|
||||
data: result.data
|
||||
};
|
||||
} catch (error) {
|
||||
if(error.response) {
|
||||
return {
|
||||
status: error.response.status,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: -1,
|
||||
headers: [],
|
||||
data: null
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = registerIpc;
|
||||
@@ -1,14 +0,0 @@
|
||||
const { ipcRenderer, contextBridge } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
on: (channel, handler) => {
|
||||
// Deliberately strip event as it includes `sender`
|
||||
const subscription = (event, ...args) => handler(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
const { customAlphabet } = require('nanoid');
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
const uuid = () => {
|
||||
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
|
||||
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
|
||||
const customNanoId = customAlphabet (urlAlphabet, 21);
|
||||
|
||||
return customNanoId();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uuid
|
||||
};
|
||||
@@ -9,6 +9,8 @@
|
||||
"packages/bruno-testbench"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"jest": "^29.2.0",
|
||||
"randomstring": "^1.2.2"
|
||||
},
|
||||
@@ -17,6 +19,8 @@
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh"
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "^12.1.0",
|
||||
"next": "12.3.1",
|
||||
"path": "^0.12.7",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^17.0.2",
|
||||
@@ -40,6 +40,7 @@
|
||||
"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",
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class QueryEditor extends React.Component {
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
readOnly: this.props.readOnly,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-Enter': () => {
|
||||
@@ -61,6 +61,8 @@ export default class QueryEditor extends React.Component {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
Tab: function (cm) {
|
||||
cm.replaceSelection(' ', 'end');
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background-color: ${(props) => props.theme.dropdown.labelBg};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
||||
@@ -8,7 +8,7 @@ const Wrapper = styled.div`
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
}
|
||||
|
||||
thead {
|
||||
@@ -29,6 +29,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -4,8 +4,11 @@ const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.environments-sidebar {
|
||||
background-color: #eaeaea;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@@ -20,15 +23,15 @@ const StyledWrapper = styled.div`
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #e4e4e4;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #dcdcdc !important;
|
||||
border-left: solid 2px var(--color-brand);
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: #dcdcdc !important;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +39,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: var(--color-text-link);
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
|
||||
@@ -9,8 +9,8 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEnvironment(environments[0]);
|
||||
}, []);
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}, [environments]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
|
||||
@@ -110,6 +110,10 @@ const Wrapper = styled.div`
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bruno-form {
|
||||
color: ${(props) => props.theme.modal.body.color};
|
||||
}
|
||||
}
|
||||
|
||||
.bruno-modal-backdrop {
|
||||
@@ -126,7 +130,7 @@ const Wrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
opacity: 0.4;
|
||||
opacity: ${(props) => props.theme.modal.backdrop.opacity};
|
||||
top: 0;
|
||||
background: black;
|
||||
position: fixed;
|
||||
@@ -140,6 +144,13 @@ const Wrapper = styled.div`
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
&.modal-footer-none {
|
||||
.bruno-modal-content {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -64,6 +64,9 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi
|
||||
if (isClosing) {
|
||||
classes += ' modal--animate-out';
|
||||
}
|
||||
if(hideFooter) {
|
||||
classes += ' modal-footer-none';
|
||||
}
|
||||
return (
|
||||
<StyledWrapper className={classes}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
|
||||
@@ -49,7 +49,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
spellCheck="false"
|
||||
onChange={(event) => onUrlChange(event.target.value)}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" onClick={handleRun}>
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.menu-icon {
|
||||
color: rgb(110 110 110);
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
.dropdown {
|
||||
div[aria-expanded='true'] {
|
||||
@@ -62,9 +62,9 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
div.dropdown-item.delete-item {
|
||||
color: var(--color-text-danger);
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: var(--color-background-danger);
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ const RemoveCollectionFromWorkspace = ({ onClose, collection }) => {
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => dispatch(removeLocalCollection(collection.uid)))
|
||||
.then(() => toast.success('Collection removed from workspace'))
|
||||
.catch((err) => console.log(err) && toast.error('An error occured while removing the collection'));
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ const Wrapper = styled.div`
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
color: rgb(110 110 110);
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
div.dropdown-item.delete-collection {
|
||||
color: var(--color-text-danger);
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
&:hover {
|
||||
background-color: var(--color-background-danger);
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
<div className="flex py-1 collection-name items-center">
|
||||
<div className="flex flex-grow items-center" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
|
||||
<div className="ml-1">{collection.name}</div>
|
||||
<div className="ml-1" id="sidebar-collection-name">{collection.name}</div>
|
||||
</div>
|
||||
<div className="collection-actions">
|
||||
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -8,6 +8,7 @@ import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LinkStyle = styled.span`
|
||||
color: ${(props) => props.theme['text-link']};
|
||||
@@ -50,8 +51,8 @@ const CreateOrAddCollection = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-2 mt-4 text-gray-600">
|
||||
{createCollectionModalOpen ? <CreateCollection handleCancel={() => setCreateCollectionModalOpen(false)} handleConfirm={handleCreateCollection} /> : null}
|
||||
<StyledWrapper className="px-2 mt-4">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} handleConfirm={handleCreateCollection} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
@@ -63,7 +64,7 @@ const CreateOrAddCollection = () => {
|
||||
<CreateLink /> or <AddLink /> Collection to Workspace.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const StyledWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Collection imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'));
|
||||
};
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Postman Collection imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div
|
||||
className='text-link hover:underline cursor-pointer'
|
||||
onClick={handleImportBrunoCollection}
|
||||
>
|
||||
Bruno Collection
|
||||
</div>
|
||||
<div
|
||||
className='text-link hover:underline cursor-pointer mt-2'
|
||||
onClick={handleImportPostmanCollection}
|
||||
>
|
||||
Postman Collection
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportCollection;
|
||||
@@ -13,6 +13,11 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.muted-message {
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const LocalCollections = ({ searchText }) => {
|
||||
<span>Open Collection</span>
|
||||
</div>
|
||||
|
||||
<div className="px-2 pt-2 text-gray-600" style={{ fontSize: 10, borderTop: 'solid 1px #e7e7e7' }}>
|
||||
<div className="px-2 pt-2 muted-message" style={{ fontSize: 10 }}>
|
||||
Note: Local collections are not tied to a workspace
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IconCode, IconFiles, IconMoon, IconChevronsLeft, IconLifebuoy } from '@
|
||||
import Link from 'next/link';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import ThemeSupport from 'components/ThemeSupport/index';
|
||||
import SwitchTheme from 'components/SwitchTheme';
|
||||
|
||||
const MenuBar = () => {
|
||||
const router = useRouter();
|
||||
@@ -23,6 +23,9 @@ const MenuBar = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full flex flex-col">
|
||||
{openBrunoSupport && <BrunoSupport onClose={() => setOpenBrunoSupport(false)} />}
|
||||
{openTheme && <SwitchTheme onClose={() => setOpenTheme(false)} />}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Link href="/">
|
||||
<div className={getClassName('/')}>
|
||||
@@ -46,18 +49,16 @@ const MenuBar = () => {
|
||||
<IconUser size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</Link> */}
|
||||
<div className="menu-item">
|
||||
<IconLifebuoy size={28} strokeWidth={1.5} onClick={() => setOpenBrunoSupport(true)} />
|
||||
<div className="menu-item" onClick={() => setOpenBrunoSupport(true)}>
|
||||
<IconLifebuoy size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
<div className="menu-item">
|
||||
<IconMoon size={28} strokeWidth={1.5} onClick={() => setOpenTheme(true)} />
|
||||
<div className="menu-item" onClick={() => setOpenTheme(true)}>
|
||||
<IconMoon size={28} strokeWidth={1.5}/>
|
||||
</div>
|
||||
<div className="menu-item" onClick={() => dispatch(toggleLeftMenuBar())}>
|
||||
<IconChevronsLeft size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
{openBrunoSupport && <BrunoSupport onClose={() => setOpenBrunoSupport(false)} />}
|
||||
{openTheme && <ThemeSupport onClose={() => setOpenTheme(false)} />}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
|
||||
aside {
|
||||
background-color: ${(props) => props.theme['sidebar-background']};
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
|
||||
.collection-title {
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.local-collection-label {
|
||||
background-color: var(--color-sidebar-background);
|
||||
}
|
||||
|
||||
.local-collections-unavailable {
|
||||
padding: 0.35rem 0.6rem;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
font-size: 11px;
|
||||
}
|
||||
.collection-dropdown {
|
||||
color: rgb(110 110 110);
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
|
||||
@@ -2,8 +2,8 @@ import toast from 'react-hot-toast';
|
||||
import Bruno from 'components/Bruno';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import importCollection from 'utils/collections/import';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
|
||||
import { IconDots } from '@tabler/icons';
|
||||
import { IconFolders } from '@tabler/icons';
|
||||
@@ -11,13 +11,13 @@ import { isElectron } from 'utils/common/platform';
|
||||
import { useState, forwardRef, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TitleBar = () => {
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
@@ -48,18 +48,10 @@ const TitleBar = () => {
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
};
|
||||
|
||||
const handleImportCollection = () => {
|
||||
importCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 py-2">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
@@ -91,7 +83,7 @@ const TitleBar = () => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
handleImportCollection();
|
||||
setImportCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Import Collection
|
||||
@@ -135,7 +127,7 @@ const TitleBar = () => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center select-none text-gray-400 text-xs local-collections-unavailable">
|
||||
<div className="flex items-center select-none text-xs local-collections-unavailable">
|
||||
Note: Local collections are only available on the desktop app.
|
||||
</div>
|
||||
)}
|
||||
|
||||
67
packages/bruno-app/src/components/SwitchTheme/index.js
Normal file
67
packages/bruno-app/src/components/SwitchTheme/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const SwitchTheme = ({ onClose }) => {
|
||||
const { storedTheme, setStoredTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
theme: storedTheme
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
setStoredTheme(values.theme);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Switch Theme'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className='bruno-form'>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="light-theme"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="light"
|
||||
checked={formik.values.theme === 'light'}
|
||||
/>
|
||||
<label htmlFor="light-theme" className="ml-1 cursor-pointer select-none">
|
||||
Light
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="dark-theme"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit()
|
||||
}}
|
||||
value="dark"
|
||||
checked={formik.values.theme === 'dark'}
|
||||
/>
|
||||
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchTheme;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const ThemeSupport = ({ onClose }) => {
|
||||
const { storedTheme, themeOptions, setStoredTheme } = useTheme();
|
||||
|
||||
console.log(themeOptions);
|
||||
|
||||
const handleThemeChange = (e) => {
|
||||
setStoredTheme(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<select name="theme_switcher" onChange={handleThemeChange} defaultValue={storedTheme}>
|
||||
{themeOptions.map((tk, index) => {
|
||||
return (
|
||||
<option value={tk} key={index}>
|
||||
{tk}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSupport;
|
||||
@@ -2,12 +2,12 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.heading {
|
||||
color: ${(props) => props.theme.welcome.heading};
|
||||
color: ${(props) => props.theme.welcome.heading};
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.welcome.muted};
|
||||
color: ${(props) => props.theme.welcome.muted};
|
||||
}
|
||||
|
||||
.collection-options {
|
||||
|
||||
@@ -10,13 +10,15 @@ import { IconBrandGithub, IconPlus, IconUpload, IconFiles, IconFolders, IconPlay
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import importCollection, { importSampleCollection } from 'utils/collections/import';
|
||||
import { importSampleCollection } from 'utils/importers/bruno-collection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
|
||||
@@ -29,15 +31,6 @@ const Welcome = () => {
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
};
|
||||
|
||||
const handleImportCollection = () => {
|
||||
importCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const handleImportSampleCollection = () => {
|
||||
importSampleCollection()
|
||||
.then((collection) => {
|
||||
@@ -58,6 +51,7 @@ const Welcome = () => {
|
||||
return (
|
||||
<StyledWrapper className="pb-4 px-6 mt-6">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
@@ -67,29 +61,29 @@ const Welcome = () => {
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Opensource API Client.</div>
|
||||
<div className="mt-4">Local-first, Opensource API Client.</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center">
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<span className="label ml-2" id="create-collection" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<IconFiles size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
<span className="label ml-2" id="add-collection" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
Add Collection to Workspace
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleImportCollection}>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconUpload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Import Collection</span>
|
||||
<span className="label ml-2" id="import-collection">Import Collection</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleImportSampleCollection}>
|
||||
<IconPlayerPlay size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Load Sample Collection</span>
|
||||
<span className="label ml-2" id="load-sample-collection">Load Sample Collection</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Modal from 'components/Modal/index';
|
||||
import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteWorkspace = ({ onClose, workspace }) => {
|
||||
@@ -14,7 +15,7 @@ const DeleteWorkspace = ({ onClose, workspace }) => {
|
||||
toast.success('Workspace deleted!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while deleting the workspace'));
|
||||
.catch(toastError);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
div:hover {
|
||||
background-color: #f4f4f4;
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const WorkspaceSelector = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setActiveWorkspace(workspaces.find((workspace) => workspace.uid === activeWorkspaceUid));
|
||||
}, [activeWorkspaceUid]);
|
||||
}, [activeWorkspaceUid, workspaces]);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -66,9 +66,9 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #545454;
|
||||
background: #efefef;
|
||||
border: solid 1px rgb(234, 234, 234);
|
||||
color: ${(props) => props.theme.button.disabled.color};
|
||||
background: ${(props) => props.theme.button.disabled.bg};
|
||||
border: solid 1px ${(props) => props.theme.button.disabled.border};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const Wrapper = styled.div`
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
padding-left: 8px;
|
||||
|
||||
@@ -28,7 +28,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-link);
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
|
||||
@@ -28,7 +28,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-link);
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
.or {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { AppProvider } from 'providers/App';
|
||||
import { AuthProvider } from 'providers/Auth';
|
||||
import { ToastProvider } from 'providers/Toaster';
|
||||
import { HotkeysProvider } from 'providers/Hotkeys';
|
||||
|
||||
import ReduxStore from 'providers/ReduxStore';
|
||||
@@ -34,12 +33,13 @@ function MyApp({ Component, pageProps }) {
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Toaster toastOptions={{ duration: 2000 }} />
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
|
||||
@@ -5,6 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { workspaceSchema } from '@usebruno/schema';
|
||||
import { findCollectionInWorkspace } from 'utils/workspaces';
|
||||
import { getWorkspacesFromIdb, saveWorkspaceToIdb, deleteWorkspaceInIdb } from 'utils/idb/workspaces';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import {
|
||||
loadWorkspaces,
|
||||
addWorkspace as _addWorkspace,
|
||||
@@ -110,7 +111,7 @@ export const deleteWorkspace = (workspaceUid) => (dispatch, getState) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (state.workspaces.activeWorkspaceUid === workspaceUid) {
|
||||
throw new Error('User cannot delete current workspace');
|
||||
throw new BrunoError('Cannot delete current workspace');
|
||||
}
|
||||
|
||||
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
|
||||
|
||||
31
packages/bruno-app/src/providers/Toaster/index.js
Normal file
31
packages/bruno-app/src/providers/Toaster/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
export const ToastContext = React.createContext();
|
||||
|
||||
export const ToastProvider = (props) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const toastOptions = { duration: 2000 };
|
||||
if(storedTheme === 'dark') {
|
||||
toastOptions.style = {
|
||||
borderRadius: '10px',
|
||||
background: '#3d3d3d',
|
||||
color: '#fff'
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider {...props} value="toastProvider">
|
||||
<Toaster toastOptions={toastOptions} />
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToastProvider;
|
||||
@@ -4,12 +4,24 @@ const darkTheme = {
|
||||
textLink: '#569cd6',
|
||||
bg: '#1e1e1e',
|
||||
|
||||
colors: {
|
||||
text: {
|
||||
danger: '#f06f57',
|
||||
muted: '#9d9d9d'
|
||||
},
|
||||
bg: {
|
||||
danger: '#d03544'
|
||||
}
|
||||
},
|
||||
|
||||
menubar: {
|
||||
bg: '#333333',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
color: '#ccc',
|
||||
muted: '#9d9d9d',
|
||||
bg: '#252526',
|
||||
|
||||
workspace: {
|
||||
bg: '#3D3D3D'
|
||||
@@ -29,6 +41,10 @@ const darkTheme = {
|
||||
indentBorder: 'solid 1px #4c4c4c'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dropdownIcon: {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,9 +57,10 @@ const darkTheme = {
|
||||
color: "rgb(204, 204, 204)",
|
||||
iconColor: "rgb(204, 204, 204)",
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
hoverBg: '#0F395E',
|
||||
hoverBg: '#185387',
|
||||
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
|
||||
seperator: '#444'
|
||||
seperator: '#444',
|
||||
labelBg: '#4a4949'
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -75,7 +92,24 @@ const darkTheme = {
|
||||
|
||||
collection: {
|
||||
environment: {
|
||||
bg: '#3D3D3D'
|
||||
bg: '#3D3D3D',
|
||||
|
||||
settings: {
|
||||
bg: '#3D3D3D',
|
||||
sidebar: {
|
||||
bg: '#3D3D3D',
|
||||
borderRight: '#4f4f4f'
|
||||
},
|
||||
item: {
|
||||
border: '#569cd6',
|
||||
hoverBg: 'transparent',
|
||||
active: {
|
||||
bg: 'transparent',
|
||||
hoverBg: 'transparent'
|
||||
},
|
||||
},
|
||||
gridBorder: '#4f4f4f'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -93,14 +127,17 @@ const darkTheme = {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
},
|
||||
backdrop: {
|
||||
opacity: 0.2
|
||||
}
|
||||
},
|
||||
|
||||
button: {
|
||||
secondary: {
|
||||
color: 'rgb(204, 204, 204)',
|
||||
bg: '#0F395E',
|
||||
border: '#0F395E',
|
||||
bg: '#185387',
|
||||
border: '#185387',
|
||||
hoverBorder: '#696969'
|
||||
},
|
||||
close: {
|
||||
@@ -108,6 +145,11 @@ const darkTheme = {
|
||||
bg: 'transparent',
|
||||
border: 'transparent',
|
||||
hoverBorder: ''
|
||||
},
|
||||
disabled: {
|
||||
color: '#a5a5a5',
|
||||
bg: '#626262',
|
||||
border: '#626262'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -154,28 +196,9 @@ const darkTheme = {
|
||||
striped: '#2A2D2F'
|
||||
},
|
||||
|
||||
'primary-text': '#ffffff',
|
||||
'secondary-text': '#929292',
|
||||
'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'
|
||||
plainGrid: {
|
||||
hoverBg: '#3D3D3D'
|
||||
}
|
||||
};
|
||||
|
||||
export default darkTheme;
|
||||
|
||||
@@ -4,12 +4,24 @@ const lightTheme = {
|
||||
textLink: '#1663bb',
|
||||
bg: '#fff',
|
||||
|
||||
colors: {
|
||||
text: {
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563',
|
||||
},
|
||||
bg: {
|
||||
danger: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
menubar: {
|
||||
bg: 'rgb(44, 44, 44)',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
muted: '#4b5563',
|
||||
bg: '#F3F3F3',
|
||||
|
||||
workspace: {
|
||||
bg: '#e1e1e1'
|
||||
@@ -29,6 +41,10 @@ const lightTheme = {
|
||||
indentBorder: 'solid 1px #d0d0d0'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dropdownIcon: {
|
||||
color: 'rgb(110 110 110)'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -43,7 +59,8 @@ const lightTheme = {
|
||||
bg: '#fff',
|
||||
hoverBg: '#e9e9e9',
|
||||
shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',
|
||||
seperator: '#e7e7e7'
|
||||
seperator: '#e7e7e7',
|
||||
labelBg: '#f3f3f3'
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -75,7 +92,28 @@ const lightTheme = {
|
||||
|
||||
collection: {
|
||||
environment: {
|
||||
bg: '#efefef'
|
||||
bg: '#efefef',
|
||||
|
||||
settings: {
|
||||
bg: 'white',
|
||||
sidebar: {
|
||||
bg: '#eaeaea',
|
||||
borderRight: 'transparent'
|
||||
},
|
||||
item: {
|
||||
border: '#546de5',
|
||||
hoverBg: '#e4e4e4',
|
||||
active: {
|
||||
bg: '#dcdcdc',
|
||||
hoverBg: '#dcdcdc'
|
||||
},
|
||||
},
|
||||
gridBorder: '#f4f4f4'
|
||||
}
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
bg: '#eaeaea'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,13 +124,16 @@ const lightTheme = {
|
||||
iconColor: 'black'
|
||||
},
|
||||
body: {
|
||||
color: '#ccc',
|
||||
color: 'rgb(52, 52, 52)',
|
||||
bg: 'white',
|
||||
},
|
||||
input : {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
},
|
||||
backdrop: {
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
|
||||
@@ -108,6 +149,11 @@ const lightTheme = {
|
||||
bg: 'white',
|
||||
border: 'white',
|
||||
hoverBorder: ''
|
||||
},
|
||||
disabled: {
|
||||
color: '#9f9f9f',
|
||||
bg: '#efefef',
|
||||
border: 'rgb(234, 234, 234)'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -153,29 +199,10 @@ const lightTheme = {
|
||||
},
|
||||
striped: '#f3f3f3'
|
||||
},
|
||||
|
||||
'primary-text': 'rgb(52 52 52)',
|
||||
'secondary-text': '#929292',
|
||||
'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'
|
||||
|
||||
plainGrid: {
|
||||
hoverBg: '#f4f4f4'
|
||||
}
|
||||
};
|
||||
|
||||
export default lightTheme;
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import toast from 'react-hot-toast';
|
||||
import { uuid } from 'utils/common';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import sampleCollection from './samples/sample-collection.json';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseJsonCollection = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let parsed = JSON.parse(str);
|
||||
return resolve(parsed);
|
||||
} catch (err) {
|
||||
toast.error('Unable to parse the collection json file');
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const validateSchema = (collection = {}) => {
|
||||
collection.uid = uuid();
|
||||
return new Promise((resolve, reject) => {
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => resolve(collection))
|
||||
.catch((err) => {
|
||||
toast.error('The Collection file is corrupted');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateUidsInCollection = (collection) => {
|
||||
collection.uid = uuid();
|
||||
|
||||
const updateItemUids = (items = []) => {
|
||||
each(items, (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
updateItemUids(collection.items);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseJsonCollection)
|
||||
.then(validateSchema)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then((collection) => {
|
||||
toast.success('Collection imported successfully');
|
||||
resolve(collection);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Import collection failed');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const importSampleCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
validateSchema(sampleCollection)
|
||||
.then(validateSchema)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
@@ -1,4 +1,4 @@
|
||||
import template from 'lodash/template';
|
||||
import reckon from 'reckonjs';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
@@ -356,21 +356,26 @@ export const isLocalCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const interpolateEnvironmentVars = (item, variables) => {
|
||||
const envVars = {};
|
||||
const { request } = item;
|
||||
const envVars = {
|
||||
interpolation: { escapeValue: true }
|
||||
};
|
||||
const _item = item.draft ? item.draft : item;
|
||||
const { request } = _item;
|
||||
each(variables, (variable) => {
|
||||
envVars[variable.name] = variable.value;
|
||||
});
|
||||
|
||||
const templateOpts = {
|
||||
interpolate: /{{([\s\S]+?)}}/g, //interpolate content using markers `{{}}`
|
||||
evaluate: null, // prevent any js evaluation
|
||||
escape: null // disable any escaping
|
||||
// TODO: Find a better interpolation library
|
||||
const interpolate = (str) => {
|
||||
if(!str || !str.length || typeof str !== "string") {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.reckon(envVars);
|
||||
};
|
||||
|
||||
const interpolate = (str) => template(str, templateOpts)(envVars);
|
||||
|
||||
request.url = interpolate(request.url);
|
||||
console.log(request.url);
|
||||
|
||||
each(request.headers, (header) => {
|
||||
header.value = interpolate(header.value);
|
||||
|
||||
@@ -9,26 +9,28 @@ export class BrunoError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export const parseError = (error) => {
|
||||
export const parseError = (error, defaultErrorMsg = 'An error occurred') => {
|
||||
if (error instanceof BrunoError) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return error.message ? error.message : 'An error occured';
|
||||
return error.message ? error.message : defaultErrorMsg;
|
||||
};
|
||||
|
||||
export const toastError = (error) => {
|
||||
export const toastError = (error, defaultErrorMsg = 'An error occurred') => {
|
||||
let errorMsg = parseError(error, defaultErrorMsg);
|
||||
|
||||
if (error instanceof BrunoError) {
|
||||
if (error.level === 'warning') {
|
||||
return toast(error.message, {
|
||||
return toast(errorMsg, {
|
||||
icon: '⚠️',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return toast.error(error.message, {
|
||||
return toast.error(errorMsg, {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
return toast.error(error.message || 'An error occured');
|
||||
return toast.error(errorMsg);
|
||||
};
|
||||
|
||||
56
packages/bruno-app/src/utils/importers/bruno-collection.js
Normal file
56
packages/bruno-app/src/utils/importers/bruno-collection.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection } from './common';
|
||||
import sampleCollection from './samples/sample-collection.json';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseJsonCollection = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let parsed = JSON.parse(str);
|
||||
return resolve(parsed);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Unable to parse the collection json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseJsonCollection)
|
||||
.then(validateSchema)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const importSampleCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
validateSchema(sampleCollection)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
44
packages/bruno-app/src/utils/importers/common.js
Normal file
44
packages/bruno-app/src/utils/importers/common.js
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
export const validateSchema = (collection = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('The Collection file is corrupted'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUidsInCollection = (_collection) => {
|
||||
const collection = cloneDeep(_collection);
|
||||
|
||||
collection.uid = uuid();
|
||||
|
||||
const updateItemUids = (items = []) => {
|
||||
each(items, (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
updateItemUids(collection.items);
|
||||
|
||||
return collection;
|
||||
};
|
||||
187
packages/bruno-app/src/utils/importers/postman-collection.js
Normal file
187
packages/bruno-app/src/utils/importers/postman-collection.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const isItemAFolder = (item) => {
|
||||
return !item.request;
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
|
||||
each(item, (i) => {
|
||||
if(isItemAFolder(i)) {
|
||||
const brunoFolderItem = {
|
||||
uid: uuid(),
|
||||
name: i.name,
|
||||
type: 'folder',
|
||||
items: []
|
||||
};
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
if(i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item);
|
||||
}
|
||||
} else {
|
||||
if(i.request) {
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: i.name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: get(i, 'request.url.raw'),
|
||||
method: i.request.method,
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const bodyMode = get(i, 'request.body.mode');
|
||||
if(bodyMode) {
|
||||
if(bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(i.request.body.formdata, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(bodyMode === 'urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(i.request.body.urlencoded, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(bodyMode === 'raw') {
|
||||
const language = get(i, 'request.body.options.raw.language');
|
||||
if(language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
} else if (language === 'xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = i.request.body.raw;
|
||||
} else {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = i.request.body.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(i.request.header, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.key,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.query'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name,
|
||||
uid: uuid(),
|
||||
version: "1",
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item);
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
|
||||
const parsePostmanCollection = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let collection = JSON.parse(str);
|
||||
let schema = get(collection, 'info.schema');
|
||||
|
||||
let v2Schemas = [
|
||||
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
];
|
||||
|
||||
if(v2Schemas.includes(schema)) {
|
||||
return resolve(importPostmanV2Collection(collection));
|
||||
}
|
||||
|
||||
throw new BrunoError('Unknown postman schema');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if(err instanceof BrunoError) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return reject(new BrunoError('Unable to parse the postman collection json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanCollection)
|
||||
.then(validateSchema)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -15,7 +15,6 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"dmg-license": "^1.0.11",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-util": "^0.17.2",
|
||||
@@ -26,6 +25,9 @@
|
||||
"nanoid": "3.3.4",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^21.1.1",
|
||||
"electron-builder": "23.0.2",
|
||||
|
||||
109
playwright.config.js
Normal file
109
playwright.config.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// @ts-check
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev:web',
|
||||
port: 3000,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
31
readme.md
31
readme.md
@@ -1,10 +1,19 @@
|
||||
# bruno
|
||||
Opensource API Client.
|
||||
Local-first, Opensource API Client.
|
||||
|
||||
### Live Demo 🏂
|
||||
Woof! Lets play with some api's [here](https://play.usebruno.com).
|
||||
Your api must allow CORS for it to work in the browser, else checkout the chrome extension ot the desktop app
|
||||
|
||||
You can visit the [Website](https://www.usebruno.com) or watch a [4 min demo](https://www.youtube.com/watch?v=wwXJW7_qyLA)
|
||||
|
||||

|
||||
|
||||
|
||||
### Bring Your Own Version Control ✨
|
||||
Bruno is built from the ground up with the Local-first paradigm in mind. This allows developers to directly store there collections on top of their local filesystem. The collections are mirrored as folders and files on the filesystem.
|
||||

|
||||
|
||||
### Comparision with Similar tools ⚖️
|
||||
Bruno is at early stages of development, and does not yet have all the bells and whistles.
|
||||
Here is a rundown of key areas where bruno is different from similar tools out there.
|
||||
@@ -20,11 +29,27 @@ Here is a rundown of key areas where bruno is different from similar tools out
|
||||
| Run in Browser | ✔️ | ✔️ | ✖️ | ✔️ |
|
||||
| Multi Tab Requests | ✔️ | ✔️ | ✖️ | ✖️ |
|
||||
|
||||
|
||||
### Contribute 👩💻🧑💻
|
||||
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
||||
|
||||
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
||||
|
||||
### Support ❤️
|
||||
Woof! If you like project, hit that ⭐ button !!
|
||||
|
||||
### Authors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Stay in touch 🌐
|
||||
[Twitter](https://twitter.com/use_bruno)
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com)
|
||||
|
||||
|
||||
### License 📄
|
||||
[MIT](license)
|
||||
[MIT](license.md)
|
||||
|
||||
41
tests/home.spec.js
Normal file
41
tests/home.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { faker } = require('@faker-js/faker');
|
||||
const { HomePage } = require('../tests/pages/home.page');
|
||||
|
||||
test.describe('bruno e2e test', () => {
|
||||
let homePage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
homePage = new HomePage(page);
|
||||
|
||||
await homePage.open();
|
||||
await expect(page).toHaveURL('/');
|
||||
await expect(page).toHaveTitle(/bruno/);
|
||||
});
|
||||
|
||||
test('user should be able to load & use sample collection', async () => {
|
||||
await homePage.loadSampleCollection();
|
||||
await expect(homePage.loadSampleCollectionToastSuccess).toBeVisible();
|
||||
|
||||
await homePage.getUsers();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
|
||||
await homePage.getSingleUser();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
|
||||
await homePage.getUserNotFound();
|
||||
await expect(homePage.statusRequestNotFound).toBeVisible();
|
||||
|
||||
await homePage.createUser();
|
||||
await expect(homePage.statusRequestCreated).toBeVisible();
|
||||
|
||||
await homePage.updateUser();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
});
|
||||
|
||||
test('user should be able to create new collection', async () => {
|
||||
await homePage.createCollection(faker.random.words());
|
||||
await expect(homePage.createCollectionToastSuccess).toBeVisible();
|
||||
})
|
||||
|
||||
});
|
||||
71
tests/pages/home.page.js
Normal file
71
tests/pages/home.page.js
Normal file
@@ -0,0 +1,71 @@
|
||||
exports.HomePage = class HomePage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
|
||||
// welcome
|
||||
this.createCollectionSelector = page.locator('#create-collection');
|
||||
this.addCollectionSelector = page.locator('#add-collection');
|
||||
this.importCollectionSelector = page.locator('#import-collection');
|
||||
this.loadSampleCollectionSelector = page.locator('#load-sample-collection');
|
||||
|
||||
// sample collection
|
||||
this.loadSampleCollectionToastSuccess = page.getByText('Sample Collection loaded successfully');
|
||||
this.sampeCollectionSelector = page.locator('#sidebar-collection-name');
|
||||
this.getUsersSelector = page.getByText('Users');
|
||||
this.getSingleUserSelector = page.getByText('Single User');
|
||||
this.getUserNotFoundSelector = page.getByText('User Not Found');
|
||||
this.postCreateSelector = page.getByText('Create');
|
||||
this.putUpdateSelector = page.getByText('Update');
|
||||
|
||||
// request panel
|
||||
this.sendRequestButton = page.locator('#send-request');
|
||||
this.statusRequestSuccess = page.getByText('200 OK');
|
||||
this.statusRequestNotFound = page.getByText('404 Not Found');
|
||||
this.statusRequestCreated = page.getByText('201 Created');
|
||||
|
||||
// create collection
|
||||
this.collectionNameField = page.locator('#collection-name');
|
||||
this.submitButton = page.locator(`button[type='submit']`);
|
||||
this.createCollectionToastSuccess = page.getByText('Collection created');
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.page.goto('/');
|
||||
}
|
||||
|
||||
async loadSampleCollection() {
|
||||
await this.loadSampleCollectionSelector.click();
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
await this.sampeCollectionSelector.click();
|
||||
await this.getUsersSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async getSingleUser() {
|
||||
await this.getSingleUserSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async getUserNotFound() {
|
||||
await this.getUserNotFoundSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
await this.postCreateSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async updateUser() {
|
||||
await this.putUpdateSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async createCollection(collectionName) {
|
||||
await this.createCollectionSelector.click();
|
||||
await this.collectionNameField.fill(collectionName);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user