mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-05 10:28:32 +00:00
Compare commits
140 Commits
feature/pr
...
v0.16.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1804454ff0 | ||
|
|
3c710120b9 | ||
|
|
fcc12fb089 | ||
|
|
3d8dee944f | ||
|
|
78e5cd3c03 | ||
|
|
26d99c7aee | ||
|
|
77a7318dfb | ||
|
|
b83da46f12 | ||
|
|
cf6ec4e84f | ||
|
|
978d810473 | ||
|
|
e83c2da798 | ||
|
|
39f148267e | ||
|
|
f4f093d4db | ||
|
|
3c4ef2f2df | ||
|
|
e39975cb3c | ||
|
|
b767ccd063 | ||
|
|
c5adfd8975 | ||
|
|
6695d90609 | ||
|
|
5c79282a1b | ||
|
|
64019f8ecf | ||
|
|
4c89f31934 | ||
|
|
1c53ce91f0 | ||
|
|
0b83fbb7ce | ||
|
|
08ceed86a8 | ||
|
|
f99918d725 | ||
|
|
1877dd858e | ||
|
|
acff0c379e | ||
|
|
a849e4fb7b | ||
|
|
613699fb69 | ||
|
|
0c4ba71922 | ||
|
|
d346bb00eb | ||
|
|
8bb57aa41d | ||
|
|
f378f04fc3 | ||
|
|
a02d2b9c58 | ||
|
|
21edfbc25a | ||
|
|
45042cd52a | ||
|
|
314e8c17d3 | ||
|
|
69a7c0e4ce | ||
|
|
626d925ad6 | ||
|
|
2c0ccf769c | ||
|
|
516411b9a2 | ||
|
|
60e3f3bb6a | ||
|
|
a6b19605b5 | ||
|
|
7ba471f26a | ||
|
|
f23dcf50a4 | ||
|
|
86cda2cf5a | ||
|
|
00b6e007af | ||
|
|
7313d1b4d7 | ||
|
|
8f803234ce | ||
|
|
76a743b74e | ||
|
|
c623aa0909 | ||
|
|
3eb26834c7 | ||
|
|
64a5852227 | ||
|
|
6471ca74c3 | ||
|
|
f77d955839 | ||
|
|
9947a55b8d | ||
|
|
a71555725c | ||
|
|
c9ec6902a5 | ||
|
|
c9c675e187 | ||
|
|
0517b2685e | ||
|
|
5d01c0a765 | ||
|
|
f3925923c9 | ||
|
|
6facdfd66b | ||
|
|
0f211131b1 | ||
|
|
cd3b8a948e | ||
|
|
f695036721 | ||
|
|
3661fa7df3 | ||
|
|
559fcb0806 | ||
|
|
d5da8a9e2f | ||
|
|
a3050db6c4 | ||
|
|
c27f090583 | ||
|
|
487dd73040 | ||
|
|
665428a2d0 | ||
|
|
6a2ba0f746 | ||
|
|
36f9902f2e | ||
|
|
c0b7dad030 | ||
|
|
8780d309ac | ||
|
|
08c1563a7a | ||
|
|
07ad1f9f60 | ||
|
|
8df6b241bb | ||
|
|
50e0558d7d | ||
|
|
cbe84cc512 | ||
|
|
cbb975d81d | ||
|
|
30ee472c40 | ||
|
|
c7aecbea79 | ||
|
|
b814c84411 | ||
|
|
6306ad17c3 | ||
|
|
4b800e30e4 | ||
|
|
89f418a114 | ||
|
|
9c8ef09d01 | ||
|
|
83d354c25c | ||
|
|
bb31ddc5d2 | ||
|
|
ff40178c8c | ||
|
|
1c549f7faf | ||
|
|
eb6b75ff98 | ||
|
|
eb010adeac | ||
|
|
7e5e22cfcf | ||
|
|
2515e78a10 | ||
|
|
511854369f | ||
|
|
18f185d37c | ||
|
|
7a0322d09e | ||
|
|
2dadad3af0 | ||
|
|
eaa31342dc | ||
|
|
c4fd9d38a5 | ||
|
|
9c4c219b99 | ||
|
|
8e22aa2fca | ||
|
|
6b9e085696 | ||
|
|
74282706aa | ||
|
|
aa88aa73a2 | ||
|
|
f78c1640e9 | ||
|
|
a5a17cf8eb | ||
|
|
c5a86cb343 | ||
|
|
9b94cddc9b | ||
|
|
0a172ddce8 | ||
|
|
aea1cbba9e | ||
|
|
7a1b44858d | ||
|
|
1c89ab3450 | ||
|
|
e3ce420216 | ||
|
|
c91fef2264 | ||
|
|
c83fce16dc | ||
|
|
5415e20d7e | ||
|
|
2f45b95930 | ||
|
|
4531cfc994 | ||
|
|
bd0738198c | ||
|
|
9a81793151 | ||
|
|
88c16fa388 | ||
|
|
f68eacfe0d | ||
|
|
116e050987 | ||
|
|
5af2f68252 | ||
|
|
a53dd76854 | ||
|
|
67fe264494 | ||
|
|
ae692dde06 | ||
|
|
1c4c5cc0c0 | ||
|
|
19ca1af71e | ||
|
|
4016a83626 | ||
|
|
71b18c8b21 | ||
|
|
b53fcbb3d1 | ||
|
|
aeb29393c5 | ||
|
|
0866d33858 | ||
|
|
ad905d1a0a |
42
.github/workflows/unit-tests.yml
vendored
42
.github/workflows/unit-tests.yml
vendored
@@ -1,29 +1,31 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
run: npm run build --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
run: npm run build --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -41,3 +41,7 @@ yarn-error.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
@@ -1,9 +1,10 @@
|
||||
## Development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Local Development
|
||||
|
||||
@@ -15,7 +16,6 @@ nvm use
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
@@ -37,5 +38,6 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"@usebruno/schema": "0.5.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -32,6 +32,7 @@
|
||||
"graphql-request": "^3.7.0",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -47,11 +48,13 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -29,7 +29,7 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<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>
|
||||
<span className="label ml-2">GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -80,7 +80,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpretted as
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: proxyConfig.enabled || false,
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().oneOf(['http', 'https']),
|
||||
hostname: Yup.string().max(1024),
|
||||
port: Yup.number().min(0).max(65535),
|
||||
auth: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string().max(1024),
|
||||
password: Yup.string().max(1024)
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled || false,
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
http
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
https
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="auth.password"
|
||||
type="text"
|
||||
name="auth.password"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
li {
|
||||
background-color: ${(props) => props.theme.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ProxySettings from './ProxySettings';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.proxy = config;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<h1 className="font-semibold mb-4">Collection Settings</h1>
|
||||
|
||||
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionSettings;
|
||||
@@ -43,7 +43,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
toast.success('Environment created in collection');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while created the environment'));
|
||||
.catch(() => toast.error('An error occurred while created the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
|
||||
toast.success('Environment deleted successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while deleting the environment'));
|
||||
.catch(() => toast.error('An error occurred while deleting the environment'));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,10 +5,22 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
padding: 4px 10px;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(4),
|
||||
&:nth-child(5) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
@@ -16,7 +28,7 @@ const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
thead td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ import React, { useReducer } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import reducer from './reducer';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
|
||||
const { variables, hasChanges } = state;
|
||||
|
||||
@@ -20,7 +23,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
type: 'CHANGES_SAVED'
|
||||
});
|
||||
})
|
||||
.catch(() => toast.error('An error occured while saving the changes'));
|
||||
.catch(() => toast.error('An error occurred while saving the changes'));
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
@@ -44,6 +47,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
variable.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
case 'secret': {
|
||||
variable.secret = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
reducerDispatch({
|
||||
type: 'UPDATE_VAR',
|
||||
@@ -63,8 +70,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -73,6 +82,14 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
? variables.map((variable, index) => {
|
||||
return (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'enabled')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
@@ -86,29 +103,25 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={variable.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'value')}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
|
||||
collection={collection}
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.secret}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'secret')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVars(variable)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ const reducer = (state, action) => {
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
});
|
||||
draft.hasChanges = true;
|
||||
@@ -24,6 +25,7 @@ const reducer = (state, action) => {
|
||||
variable.name = action.variable.name;
|
||||
variable.value = action.variable.value;
|
||||
variable.enabled = action.variable.enabled;
|
||||
variable.secret = action.variable.secret;
|
||||
draft.hasChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
toast.success('Environment renamed successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while renaming the environment'));
|
||||
.catch(() => toast.error('An error occurred while renaming the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const Wrapper = styled.div`
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
z-index: 1003;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bruno-modal-card {
|
||||
@@ -28,7 +28,7 @@ const Wrapper = styled.div`
|
||||
background: var(--color-background-top);
|
||||
border-radius: var(--border-radius);
|
||||
position: relative;
|
||||
z-index: 1003;
|
||||
z-index: 10;
|
||||
max-width: calc(100% - var(--spacing-base-unit));
|
||||
box-shadow: var(--box-shadow-base);
|
||||
display: flex;
|
||||
|
||||
@@ -3,7 +3,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ModalHeader = ({ title, handleCancel }) => (
|
||||
<div className="bruno-modal-header">
|
||||
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
|
||||
{title ? <div className="bruno-modal-header-title">{title}</div> : null}
|
||||
{handleCancel ? (
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
|
||||
×
|
||||
|
||||
@@ -27,7 +27,7 @@ const Support = () => {
|
||||
<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>
|
||||
<span className="label ml-2">GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -114,7 +114,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
|
||||
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>;
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
@@ -125,7 +125,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ const useGraphqlSchema = (endpoint, environment) => {
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading GraphQL Schema');
|
||||
toast.error('Error occurred while loading GraphQL Schema');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
|
||||
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>;
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
@@ -73,7 +73,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Query
|
||||
</div>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpretted as
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -72,19 +74,28 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{headers && headers.length
|
||||
? headers.map((header, index) => {
|
||||
? headers.map((header) => {
|
||||
return (
|
||||
<tr key={header.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={header.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -13,6 +13,8 @@ import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import VariablesEditor from 'components/VariablesEditor';
|
||||
import CollectionSettings from 'components/CollectionSettings';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -110,7 +112,7 @@ const RequestTabPanel = () => {
|
||||
}
|
||||
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
|
||||
return <div className="pb-4 px-4">An error occured!</div>;
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
|
||||
@@ -118,11 +120,18 @@ const RequestTabPanel = () => {
|
||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
const showRunner = collection.showRunner;
|
||||
if (showRunner) {
|
||||
if (focusedTab.type === 'collection-runner') {
|
||||
return <RunnerResults collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'variables') {
|
||||
return <VariablesEditor collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'collection-settings') {
|
||||
return <CollectionSettings collection={collection} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { IconFiles, IconRun } from '@tabler/icons';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import VariablesView from 'components/VariablesView';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionToolBar = ({ collection }) => {
|
||||
@@ -11,8 +11,30 @@ const CollectionToolBar = ({ collection }) => {
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(
|
||||
toggleRunnerView({
|
||||
collectionUid: collection.uid
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const viewVariables = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'variables'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -28,7 +50,12 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
<VariablesView collection={collection} />
|
||||
<span className="mr-3">
|
||||
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
</span>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
const getTabInfo = (type) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Settings</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return (
|
||||
<>
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Variables</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-runner': {
|
||||
return (
|
||||
<>
|
||||
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Runner</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialTab;
|
||||
@@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -56,6 +57,14 @@ const RequestTab = ({ tab, collection }) => {
|
||||
return color;
|
||||
};
|
||||
|
||||
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, tab.uid);
|
||||
|
||||
if (!item) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.borromBorder};
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
|
||||
@@ -76,9 +76,7 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showRunner = activeCollection && activeCollection.showRunner;
|
||||
|
||||
// Todo: Must support ephermal requests
|
||||
// Todo: Must support ephemeral requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
{newRequestModalOpen && (
|
||||
@@ -87,72 +85,70 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
{!showRunner ? (
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li
|
||||
key={tab.uid}
|
||||
className={getTabClassname(tab, index)}
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<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>
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li
|
||||
key={tab.uid}
|
||||
className={getTabClassname(tab, index)}
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
) : null}
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getContentType, formatResponse } from 'utils/common';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
import Overlay from './Overlay';
|
||||
@@ -41,9 +41,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={
|
||||
response.data ? (isJson(response.headers) ? safeStringifyJSON(response.data, true) : response.data) : ''
|
||||
}
|
||||
value={response.data ? formatResponse(response) : ''}
|
||||
mode={getContentType(response.headers)}
|
||||
/>
|
||||
);
|
||||
@@ -95,31 +93,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getContentType = (headers) => {
|
||||
if (headers && headers.length) {
|
||||
let contentType = headers
|
||||
.filter((header) => header[0].toLowerCase() === 'content-type')
|
||||
.map((header) => {
|
||||
return header[1];
|
||||
});
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const isJson = (headers) => {
|
||||
return getContentType(headers) === 'application/ld+json';
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import slash from 'utils/common/slash';
|
||||
@@ -69,9 +69,9 @@ export default function RunnerResults({ collection }) {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
};
|
||||
|
||||
const closeRunner = () => {
|
||||
const resetRunner = () => {
|
||||
dispatch(
|
||||
closeCollectionRunner({
|
||||
resetCollectionRunner({
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
@@ -101,8 +101,8 @@ export default function RunnerResults({ collection }) {
|
||||
Run Collection
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
@@ -202,8 +202,8 @@ export default function RunnerResults({ collection }) {
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
|
||||
Close
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -28,7 +28,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occured while cloning the request');
|
||||
toast.error(err ? err.message : 'An error occurred while cloning the request');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -12,8 +13,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
showRunnerView({
|
||||
collectionUid: collection.uid
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -86,11 +86,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(
|
||||
hideRunnerView({
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
if (isItemARequest(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
function countRequests(items) {
|
||||
let count = 0;
|
||||
|
||||
function recurse(item) {
|
||||
if (item && typeof item === 'object') {
|
||||
if (item.type !== 'folder') {
|
||||
count++;
|
||||
}
|
||||
if (Array.isArray(item.items)) {
|
||||
item.items.forEach(recurse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(recurse);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
const CollectionProperties = ({ collection, onClose }) => {
|
||||
return (
|
||||
<Modal size="sm" title="Collection Properties" hideFooter={true} handleCancel={onClose}>
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Name :</td>
|
||||
<td className="py-2 px-2">{collection.name}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Location :</td>
|
||||
<td className="py-2 px-2 break-all">{collection.pathname}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Environments :</td>
|
||||
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Requests :</td>
|
||||
<td className="py-2 px-2">{countRequests(collection.items)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionProperties;
|
||||
@@ -13,7 +13,7 @@ const RemoveCollection = ({ onClose, collection }) => {
|
||||
toast.success('Collection removed');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while removing the collection'));
|
||||
.catch(() => toast.error('An error occurred while removing the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDrop } from 'react-dnd';
|
||||
@@ -8,11 +9,12 @@ import Dropdown from 'components/Dropdown';
|
||||
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import RunCollectionItem from './CollectionItem/RunCollectionItem';
|
||||
import CollectionProperties from './CollectionProperties';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
@@ -25,7 +27,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [showRunCollectionModal, setShowRunCollectionModal] = useState(false);
|
||||
const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -39,6 +41,16 @@ const Collection = ({ collection, searchText }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setCollectionIsCollapsed(false);
|
||||
@@ -103,8 +115,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showRemoveCollectionModal && (
|
||||
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
|
||||
)}
|
||||
{showRunCollectionModal && (
|
||||
<RunCollectionItem collection={collection} onClose={() => setShowRunCollectionModal(false)} />
|
||||
{collectionPropertiesModal && (
|
||||
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
|
||||
)}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
|
||||
@@ -142,7 +154,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRunCollectionModal(true);
|
||||
handleRun();
|
||||
}}
|
||||
>
|
||||
Run
|
||||
@@ -165,6 +177,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
Export
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setCollectionPropertiesModal(true);
|
||||
}}
|
||||
>
|
||||
Properties
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ const CreateOrOpenCollection = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occured while opening the collection')
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
);
|
||||
};
|
||||
const CreateLink = () => (
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconSearch, IconFolders, IconSortAZ } from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const CollectionsBadge = () => {
|
||||
const dispatch = useDispatch()
|
||||
return (
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
<div className='collections-badge flex items-center justify-between px-2' >
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
<button onClick={() => dispatch(sortCollections())} >
|
||||
<IconSortAZ size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -64,12 +71,12 @@ const Collections = () => {
|
||||
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
</DndProvider>
|
||||
);
|
||||
})
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
</DndProvider>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -27,8 +27,11 @@ const CreateCollection = ({ onClose }) => {
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string().required('location is required')
|
||||
collectionLocation: Yup.string()
|
||||
.min(1, 'location is required')
|
||||
.required('location is required'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
@@ -36,14 +39,17 @@ const CreateCollection = ({ onClose }) => {
|
||||
toast.success('Collection created');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while creating the collection'));
|
||||
.catch(() => toast.error('An error occurred while creating the collection'));
|
||||
}
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
// When the user closes the diolog without selecting anything dirPath will be false
|
||||
if (typeof dirPath === 'string') {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
@@ -63,9 +69,8 @@ const CreateCollection = ({ onClose }) => {
|
||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="flex items-center">
|
||||
<span className="font-semibold">Name</span>
|
||||
<Tooltip text="Name of the collection" tooltipId="collection-name" />
|
||||
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
@@ -84,9 +89,34 @@ const CreateCollection = ({ onClose }) => {
|
||||
<div className="text-red-500">{formik.errors.collectionName}</div>
|
||||
) : null}
|
||||
|
||||
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
|
||||
<label htmlFor="collection-location" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name" />
|
||||
<Tooltip text="This folder will be created under the selected location" tooltipId="collection-folder-name-tooltip" />
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
@@ -103,34 +133,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -32,7 +32,7 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
|
||||
.then(() => onClose())
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import toast from 'react-hot-toast';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
@@ -34,10 +34,10 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (isEphermal) {
|
||||
if (isEphemeral) {
|
||||
const uid = uuid();
|
||||
dispatch(
|
||||
newEphermalHttpRequest({
|
||||
newEphemeralHttpRequest({
|
||||
uid: uid,
|
||||
requestName: values.requestName,
|
||||
requestType: values.requestType,
|
||||
@@ -56,7 +56,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
);
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
} else {
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
@@ -69,7 +69,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ const TitleBar = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occured while opening the collection')
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ const Sidebar = () => {
|
||||
</GitHubButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.14.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.6</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
|
||||
@@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
|
||||
const word = /[\w$-]+/;
|
||||
const wordlist = (options && options.autocomplete) || [];
|
||||
let cur = editor.getCursor(),
|
||||
curLine = editor.getLine(cur.line);
|
||||
let end = cur.ch,
|
||||
start = end;
|
||||
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && curLine.slice(start, end);
|
||||
|
||||
// Check if curWord is a valid string before proceeding
|
||||
if (typeof curWord !== 'string' || curWord.length < 3) {
|
||||
return null; // Abort the hint
|
||||
}
|
||||
|
||||
const list = (options && options.list) || [];
|
||||
const re = new RegExp(word.source, 'g');
|
||||
for (let dir = -1; dir <= 1; dir += 2) {
|
||||
let line = cur.line,
|
||||
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
|
||||
for (; line != endLine; line += dir) {
|
||||
let text = editor.getLine(line),
|
||||
m;
|
||||
while ((m = re.exec(text))) {
|
||||
if (line == cur.line && curWord.length < 3) continue;
|
||||
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
|
||||
});
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
@@ -31,6 +65,7 @@ class SingleLineEditor extends Component {
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
@@ -72,6 +107,14 @@ class SingleLineEditor extends Component {
|
||||
Tab: () => {}
|
||||
}
|
||||
});
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
|
||||
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.setValue(this.props.value || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
@@ -87,7 +130,7 @@ class SingleLineEditor extends Component {
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpretted as
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
li {
|
||||
background-color: ${(props) => props.theme.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
98
packages/bruno-app/src/components/VariablesEditor/index.js
Normal file
98
packages/bruno-app/src/components/VariablesEditor/index.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { Inspector } from 'react-inspector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const KeyValueExplorer = ({ data, theme }) => {
|
||||
data = data || {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="border-collapse">
|
||||
<tbody>
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td className="px-2 py-1">{key}</td>
|
||||
<td className="px-2 py-1">
|
||||
<Inspector data={value} theme={theme} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvVariables = ({ collection, theme }) => {
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
if (!environment) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
|
||||
<div className="muted text-xs">No environment selected</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const envVars = get(environment, 'variables', []);
|
||||
const enabledEnvVars = filter(envVars, (variable) => variable.enabled);
|
||||
const envVarsObj = enabledEnvVars.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center mt-4 mb-2">
|
||||
<h1 className="font-semibold">Environment Variables</h1>
|
||||
<span className="muted ml-2">({environment.name})</span>
|
||||
</div>
|
||||
{enabledEnvVars.length > 0 ? (
|
||||
<KeyValueExplorer data={envVarsObj} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No environment variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionVariables = ({ collection, theme }) => {
|
||||
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mb-2">Collection Variables</h1>
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No collection variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const VariablesEditor = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, collection variables can only be set via the API -{' '}
|
||||
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify collection variables.
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesEditor;
|
||||
@@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
min-width: fit-content;
|
||||
font-size: 14px;
|
||||
top: 36px;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
background-color: ${(props) => props.theme.variables.bg};
|
||||
|
||||
.popover {
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useOnClickOutside from 'hooks/useOnClickOutside';
|
||||
|
||||
const PopOver = ({ children, iconRef, handleClose }) => {
|
||||
const popOverRef = useRef(null);
|
||||
|
||||
useOnClickOutside(popOverRef, (e) => {
|
||||
if (iconRef && iconRef.current) {
|
||||
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="popover" ref={popOverRef}>
|
||||
<div className="popover-content">{children}</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopOver;
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.view-environment {
|
||||
width: 1rem;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.variable-name {
|
||||
color: ${(props) => props.theme.variables.name.color};
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
max-width: 600px;
|
||||
inline-size: 600px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
const collectionVars = [];
|
||||
|
||||
forOwn(cloneDeep(collectionVariables), (value, key) => {
|
||||
collectionVars.push({
|
||||
uid: uuid(),
|
||||
name: key,
|
||||
value: value
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="mb-2 font-medium">Environment Variables</div>
|
||||
{variables && variables.length ? (
|
||||
variables.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
|
||||
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<small>No env variables found</small>
|
||||
)}
|
||||
|
||||
<div className="mt-2 font-medium">Collection Variables</div>
|
||||
{collectionVars && collectionVars.length ? (
|
||||
collectionVars.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
|
||||
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<small>No collection variables found</small>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesTable;
|
||||
@@ -1,48 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import VariablesTable from './VariablesTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import PopOver from './Popover';
|
||||
import { IconEye } from '@tabler/icons';
|
||||
|
||||
const VariablesView = ({ collection }) => {
|
||||
const iconRef = useRef(null);
|
||||
const [popOverOpen, setPopOverOpen] = useState(false);
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const variables = get(environment, 'variables', []);
|
||||
const enabledVariables = filter(variables, (variable) => variable.enabled);
|
||||
const showVariablesTable =
|
||||
enabledVariables.length > 0 ||
|
||||
(collection.collectionVariables && Object.keys(collection.collectionVariables).length > 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mr-2 server-syncstatus-icon" ref={iconRef}>
|
||||
<div
|
||||
className="flex p-1 items-center"
|
||||
onClick={() => setPopOverOpen(true)}
|
||||
onMouseEnter={() => setPopOverOpen(true)}
|
||||
onMouseLeave={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className="cursor-pointer view-environment">
|
||||
<IconEye size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
{popOverOpen && (
|
||||
<PopOver iconRef={iconRef} handleClose={() => setPopOverOpen(false)}>
|
||||
<div className="px-2 py-1">
|
||||
{showVariablesTable ? (
|
||||
<VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables} />
|
||||
) : (
|
||||
'No variables found'
|
||||
)}
|
||||
</div>
|
||||
</PopOver>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesView;
|
||||
@@ -19,7 +19,7 @@ const Welcome = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occured while opening the collection')
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ const Welcome = () => {
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
@@ -93,7 +93,7 @@ const Welcome = () => {
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Github</span>
|
||||
<span className="label ml-2">GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
componentDidMount() {
|
||||
// Add a global error event listener to capture client-side errors
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
this.setState({ hasError: true, error });
|
||||
};
|
||||
}
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.log({ error, errorInfo });
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10">
|
||||
<div className="bg-white rounded-lg shadow-lg p-4 w-full">
|
||||
<h1 className="text-2xl font-semibold text-red-600 mb-2">Oops! Something went wrong</h1>
|
||||
<p className="text-red-600 mb-2">{this.state.error && this.state.error.toString()}</p>
|
||||
{this.state.error && this.state.error.stack && (
|
||||
<pre className="bg-gray-100 p-2 rounded-lg overflow-auto">{this.state.error.stack}</pre>
|
||||
)}
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition"
|
||||
onClick={() => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
|
||||
|
||||
import ReduxStore from 'providers/ReduxStore';
|
||||
import ThemeProvider from 'providers/Theme/index';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
import '../styles/app.scss';
|
||||
import '../styles/globals.css';
|
||||
@@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<PreferencesProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</PreferencesProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
</SafeHydrate>
|
||||
<ErrorBoundary>
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<PreferencesProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</PreferencesProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
</SafeHydrate>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
processEnvUpdateEvent,
|
||||
collectionRenamedEvent,
|
||||
runRequestEvent,
|
||||
runFolderEvent
|
||||
runFolderEvent,
|
||||
brunoConfigUpdateEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -26,8 +28,8 @@ const useCollectionTreeSync = () => {
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const _openCollection = (pathname, uid, name) => {
|
||||
dispatch(openCollectionEvent(uid, pathname, name));
|
||||
const _openCollection = (pathname, uid, brunoConfig) => {
|
||||
dispatch(openCollectionEvent(uid, pathname, brunoConfig));
|
||||
};
|
||||
|
||||
const _collectionTreeUpdated = (type, val) => {
|
||||
@@ -97,6 +99,10 @@ const useCollectionTreeSync = () => {
|
||||
dispatch(scriptEnvironmentUpdateEvent(val));
|
||||
};
|
||||
|
||||
const _processEnvUpdate = (val) => {
|
||||
dispatch(processEnvUpdateEvent(val));
|
||||
};
|
||||
|
||||
const _collectionRenamed = (val) => {
|
||||
dispatch(collectionRenamedEvent(val));
|
||||
};
|
||||
@@ -119,9 +125,11 @@ const useCollectionTreeSync = () => {
|
||||
const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
||||
const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
|
||||
const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent);
|
||||
const removeListener9 = ipcRenderer.on('main:console-log', (val) => {
|
||||
const removeListener9 = ipcRenderer.on('main:process-env-update', _processEnvUpdate);
|
||||
const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
});
|
||||
const removeListener11 = ipcRenderer.on('main:bruno-config-update', (val) => dispatch(brunoConfigUpdateEvent(val)));
|
||||
|
||||
return () => {
|
||||
removeListener1();
|
||||
@@ -133,6 +141,8 @@ const useCollectionTreeSync = () => {
|
||||
removeListener7();
|
||||
removeListener8();
|
||||
removeListener9();
|
||||
removeListener10();
|
||||
removeListener11();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, saveRequest, collections]);
|
||||
|
||||
// edit environmentss (ctrl/cmd + e)
|
||||
// edit environments (ctrl/cmd + e)
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
renameItem as _renameItem,
|
||||
cloneItem as _cloneItem,
|
||||
deleteItem as _deleteItem,
|
||||
sortCollections as _sortCollections,
|
||||
saveRequest as _saveRequest,
|
||||
selectEnvironment as _selectEnvironment,
|
||||
createCollection as _createCollection,
|
||||
@@ -42,8 +43,8 @@ import {
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||
} from './index';
|
||||
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
@@ -262,7 +263,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
||||
ipcRenderer
|
||||
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/251
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -346,13 +359,25 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/265
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_deleteItem({ itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
export const sortCollections = () => (dispatch) => {
|
||||
dispatch(_sortCollections());
|
||||
};
|
||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@@ -723,11 +748,7 @@ export const removeCollection = (collectionUid) => (dispatch, getState) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:remove-collection', collection.pathname)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: recursivelyGetAllItemUids(collection.items)
|
||||
})
|
||||
);
|
||||
dispatch(closeAllCollectionTabs({ collectionUid }));
|
||||
})
|
||||
.then(waitForNextTick)
|
||||
.then(() => {
|
||||
@@ -750,15 +771,31 @@ export const browseDirectory = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState) => {
|
||||
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {
|
||||
const collection = {
|
||||
version: '1',
|
||||
uid: uid,
|
||||
name: name,
|
||||
name: brunoConfig.name,
|
||||
pathname: pathname,
|
||||
items: [],
|
||||
showRunner: false,
|
||||
collectionVariables: {}
|
||||
collectionVariables: {},
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -52,6 +52,14 @@ export const collectionsSlice = createSlice({
|
||||
state.collections.push(collection);
|
||||
}
|
||||
},
|
||||
brunoConfigUpdateEvent: (state, action) => {
|
||||
const { collectionUid, brunoConfig } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.brunoConfig = brunoConfig;
|
||||
}
|
||||
},
|
||||
renameCollection: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -62,6 +70,9 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
sortCollections: (state) => {
|
||||
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name))
|
||||
},
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -177,6 +188,14 @@ export const collectionsSlice = createSlice({
|
||||
collection.collectionVariables = collectionVariables;
|
||||
}
|
||||
},
|
||||
processEnvUpdateEvent: (state, action) => {
|
||||
const { collectionUid, processEnvVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.processEnvVariables = processEnvVariables;
|
||||
}
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
const { itemUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -213,7 +232,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
newEphermalHttpRequest: (state, action) => {
|
||||
newEphemeralHttpRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection && collection.items && collection.items.length) {
|
||||
@@ -1000,30 +1019,6 @@ export const collectionsSlice = createSlice({
|
||||
collection.name = newName;
|
||||
}
|
||||
},
|
||||
toggleRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = !collection.showRunner;
|
||||
}
|
||||
},
|
||||
showRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = true;
|
||||
}
|
||||
},
|
||||
hideRunnerView: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.showRunner = false;
|
||||
}
|
||||
},
|
||||
resetRunResults: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -1133,13 +1128,12 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
closeCollectionRunner: (state, action) => {
|
||||
resetCollectionRunner: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.showRunner = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1147,8 +1141,10 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
export const {
|
||||
createCollection,
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
@@ -1158,10 +1154,11 @@ export const {
|
||||
renameItem,
|
||||
cloneItem,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
processEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
saveRequest,
|
||||
newEphermalHttpRequest,
|
||||
newEphemeralHttpRequest,
|
||||
collectionClicked,
|
||||
collectionFolderClicked,
|
||||
requestUrlChanged,
|
||||
@@ -1198,13 +1195,10 @@ export const {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
collectionRenamedEvent,
|
||||
toggleRunnerView,
|
||||
showRunnerView,
|
||||
hideRunnerView,
|
||||
resetRunResults,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
closeCollectionRunner
|
||||
resetCollectionRunner
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -10,6 +10,10 @@ const initialState = {
|
||||
activeTabUid: null
|
||||
};
|
||||
|
||||
const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
|
||||
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
|
||||
};
|
||||
|
||||
export const tabsSlice = createSlice({
|
||||
name: 'tabs',
|
||||
initialState,
|
||||
@@ -19,12 +23,22 @@ export const tabsSlice = createSlice({
|
||||
if (alreadyExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
|
||||
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
|
||||
if (tab) {
|
||||
state.activeTabUid = tab.uid;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.tabs.push({
|
||||
uid: action.payload.uid,
|
||||
collectionUid: action.payload.collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response'
|
||||
responsePaneTab: 'response',
|
||||
type: action.payload.type || 'request'
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
@@ -55,16 +69,22 @@ export const tabsSlice = createSlice({
|
||||
closeTabs: (state, action) => {
|
||||
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
const tabUids = action.payload.tabUids || [];
|
||||
|
||||
// remove the tabs from the state
|
||||
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
|
||||
|
||||
if (activeTab && state.tabs.length) {
|
||||
const { collectionUid } = activeTab;
|
||||
const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
|
||||
// if the active tab no longer exists, set the active tab to the last tab in the list
|
||||
// this implies that the active tab was closed
|
||||
if (!activeTabStillExists) {
|
||||
// attempt to load sibling tabs (based on collections) of the dead tab
|
||||
// load sibling tabs of the current collection
|
||||
const siblingTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);
|
||||
|
||||
// if there are sibling tabs, set the active tab to the last sibling tab
|
||||
// otherwise, set the active tab to the last tab in the list
|
||||
if (siblingTabs && siblingTabs.length) {
|
||||
state.activeTabUid = last(siblingTabs).uid;
|
||||
} else {
|
||||
@@ -76,11 +96,23 @@ export const tabsSlice = createSlice({
|
||||
if (!state.tabs || !state.tabs.length) {
|
||||
state.activeTabUid = null;
|
||||
}
|
||||
},
|
||||
closeAllCollectionTabs: (state, action) => {
|
||||
const collectionUid = action.payload.collectionUid;
|
||||
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
|
||||
state.activeTabUid = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { addTab, focusTab, updateRequestPaneTabWidth, updateRequestPaneTab, updateResponsePaneTab, closeTabs } =
|
||||
tabsSlice.actions;
|
||||
export const {
|
||||
addTab,
|
||||
focusTab,
|
||||
updateRequestPaneTabWidth,
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
closeTabs,
|
||||
closeAllCollectionTabs
|
||||
} = tabsSlice.actions;
|
||||
|
||||
export default tabsSlice.reducer;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "buttons";
|
||||
@import 'buttons';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
:root {
|
||||
--color-brand: #546de5;
|
||||
--color-text: rgb(52 52 52);
|
||||
@@ -21,7 +20,8 @@
|
||||
--color-method-head: rgb(52 52 52);
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
@@ -38,15 +38,18 @@ body {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar, .CodeMirror-vscrollbar::-webkit-scrollbar {
|
||||
body::-webkit-scrollbar,
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar {
|
||||
width: 0.6rem;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track, .CodeMirror-vscrollbar::-webkit-scrollbar-track {
|
||||
|
||||
body::-webkit-scrollbar-track,
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb, .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
|
||||
|
||||
body::-webkit-scrollbar-thumb,
|
||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cdcdcd;
|
||||
border-radius: 5rem;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ const darkTheme = {
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
hoverBg: '#185387',
|
||||
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
|
||||
seperator: '#444',
|
||||
separator: '#444',
|
||||
labelBg: '#4a4949'
|
||||
},
|
||||
|
||||
@@ -174,7 +174,7 @@ const darkTheme = {
|
||||
requestTabs: {
|
||||
color: '#ccc',
|
||||
bg: '#2A2D2F',
|
||||
borromBorder: '#444',
|
||||
bottomBorder: '#444',
|
||||
icon: {
|
||||
color: '#9f9f9f',
|
||||
hoverColor: 'rgb(204, 204, 204)',
|
||||
|
||||
@@ -70,7 +70,7 @@ 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',
|
||||
separator: '#e7e7e7',
|
||||
labelBg: '#f3f3f3'
|
||||
},
|
||||
|
||||
@@ -178,7 +178,7 @@ const lightTheme = {
|
||||
requestTabs: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
bg: '#f7f7f7',
|
||||
borromBorder: '#efefef',
|
||||
bottomBorder: '#efefef',
|
||||
icon: {
|
||||
color: '#9f9f9f',
|
||||
hoverColor: 'rgb(76 76 76)',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const { get } = require('lodash');
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
@@ -20,7 +21,7 @@ if (!SERVER_RENDERED) {
|
||||
// str is of format {{variableName}}, extract variableName
|
||||
// we are seeing that from the gql query editor, the token string is of format variableName
|
||||
const variableName = str.replace('{{', '').replace('}}', '').trim();
|
||||
const variableValue = options.variables[variableName];
|
||||
const variableValue = get(options.variables, variableName);
|
||||
|
||||
const into = document.createElement('div');
|
||||
const descriptionDiv = document.createElement('div');
|
||||
@@ -65,8 +66,7 @@ if (!SERVER_RENDERED) {
|
||||
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.className !== 'cm-variable-valid') {
|
||||
if (!target.classList.contains('cm-variable-valid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,22 @@ const deleteUidsInEnvs = (envs) => {
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSecretsInEnvs = (envs) => {
|
||||
each(envs, (env) => {
|
||||
each(env.variables, (variable) => {
|
||||
if (variable.secret) {
|
||||
variable.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const exportCollection = (collection) => {
|
||||
// delete uids
|
||||
delete collection.uid;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
deleteSecretsInEnvs(collection.environments);
|
||||
transformItem(collection.items);
|
||||
|
||||
const fileName = `${collection.name}.json`;
|
||||
|
||||
@@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
@@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
@@ -542,6 +546,11 @@ export const getAllVariables = (collection) => {
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collection.collectionVariables
|
||||
...collection.collectionVariables,
|
||||
process: {
|
||||
env: {
|
||||
...collection.processEnvVariables
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import get from 'lodash/get';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
@@ -5,6 +8,11 @@ if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
const pathFoundInVariables = (path, obj) => {
|
||||
const value = get(obj, path);
|
||||
return isString(value);
|
||||
};
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||
let variablesOverlay = {
|
||||
@@ -15,11 +23,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch == '}' && stream.next() == '}') {
|
||||
stream.eat('}');
|
||||
if (word in variables) {
|
||||
return 'variable-valid';
|
||||
let found = pathFoundInVariables(word, variables);
|
||||
if (found) {
|
||||
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||
} else {
|
||||
return 'variable-invalid';
|
||||
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||
}
|
||||
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
|
||||
}
|
||||
word += ch;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
export const uuid = () => {
|
||||
@@ -61,3 +62,34 @@ export const normalizeFileName = (name) => {
|
||||
|
||||
return formattedName;
|
||||
};
|
||||
|
||||
export const getContentType = (headers) => {
|
||||
if (headers && headers.length) {
|
||||
let contentType = headers
|
||||
.filter((header) => header[0].toLowerCase() === 'content-type')
|
||||
.map((header) => {
|
||||
return header[1];
|
||||
});
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType[0];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const formatResponse = (response) => {
|
||||
let type = getContentType(response.headers);
|
||||
if (type.includes('json')) {
|
||||
return safeStringifyJSON(response.data, true);
|
||||
}
|
||||
if (type.includes('xml')) {
|
||||
return xmlFormat(response.data, { collapseContent: true });
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
import platform from 'platform';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
|
||||
|
||||
return path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('windows');
|
||||
};
|
||||
|
||||
@@ -30,10 +30,23 @@ const parseGraphQL = (text) => {
|
||||
}
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request) => {
|
||||
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||
// Check if the request name already exist and if so add a number suffix
|
||||
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||
if (otherItem.name === item.name && otherIndex < index) {
|
||||
nameSuffix++;
|
||||
}
|
||||
return nameSuffix;
|
||||
}, 0);
|
||||
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||
}
|
||||
|
||||
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: request.name,
|
||||
name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
@@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => {
|
||||
try {
|
||||
const insomniaExport = JSON.parse(data);
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find(
|
||||
(resource) => resource._type === 'workspace' && resource.scope === 'collection'
|
||||
);
|
||||
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||
@@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => {
|
||||
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder) => {
|
||||
const folders = requestGroups.map((folder, index, allFolder) => {
|
||||
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||
const requests = resources.filter(
|
||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||
);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: folder.name,
|
||||
name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||
};
|
||||
|
||||
@@ -91,7 +91,10 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
|
||||
}
|
||||
|
||||
if (bodyMode === 'raw') {
|
||||
const language = get(i, 'request.body.options.raw.language');
|
||||
let language = get(i, 'request.body.options.raw.language');
|
||||
if (!language) {
|
||||
language = searchLanguageByHeader(i.request.header);
|
||||
}
|
||||
if (language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
@@ -131,6 +134,21 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
|
||||
});
|
||||
};
|
||||
|
||||
const searchLanguageByHeader = (headers) => {
|
||||
let contentType;
|
||||
each(headers, (header) => {
|
||||
if (header.key.toLowerCase() === 'content-type' && !header.disabled) {
|
||||
if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) {
|
||||
contentType = 'json';
|
||||
} else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) {
|
||||
contentType = 'xml';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name,
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 0.10.1
|
||||
|
||||
- fix(#233) Fixed Issue related to content header parsing
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Support for proxying requests through a proxy server
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- `--output` flag to collect the results of your API tests
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- `--env-var` flag to set environment variables
|
||||
- loading environment variables from `.env` file
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* `--cacert` flag to support custom CA certificates
|
||||
- `--cacert` flag to support custom CA certificates
|
||||
|
||||
## 0.7.0
|
||||
|
||||
* `--insecure` flag to disable SSL verification
|
||||
- `--insecure` flag to disable SSL verification
|
||||
|
||||
238
packages/bruno-cli/examples/report.json
Normal file
238
packages/bruno-cli/examples/report.json
Normal file
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"summary": {
|
||||
"totalAssertions": 4,
|
||||
"passedAssertions": 4,
|
||||
"failedAssertions": 0,
|
||||
"totalTests": 0,
|
||||
"passedTests": 0,
|
||||
"failedTests": 0
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v4",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-08gGpUcq2NTnMCVT5AuXxQ0DzGE\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v4",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
},
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "mTrKBl5YU6jiAVG-phKT4",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v2",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-lMqxZgVOJiQXjF5yk3AFEU8O9Ro\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v2",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
},
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "XsjjGx9cjt5t8tE_t69ZB",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8080/test/v3",
|
||||
"headers": {}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "497",
|
||||
"etag": "W/\"1f1-tSiYu0/vWz3r+NYRCaed0aW1waw\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v3",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "GET",
|
||||
"body": "",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {}
|
||||
}
|
||||
},
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "i_8MmDMtJA9YfvB_FrW15",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:8080/test/v1",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"data": {
|
||||
"test": "hello"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"headers": {
|
||||
"x-powered-by": "Express",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": "623",
|
||||
"etag": "W/\"26f-ku5QGz4p9f02u79vJIve7JH3QYM\"",
|
||||
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
|
||||
"connection": "close"
|
||||
},
|
||||
"data": {
|
||||
"path": "/test/v1",
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "axios/1.5.0",
|
||||
"content-length": "16",
|
||||
"accept-encoding": "gzip, compress, deflate, br",
|
||||
"host": "localhost:8080",
|
||||
"connection": "close"
|
||||
},
|
||||
"method": "POST",
|
||||
"body": "{\"test\":\"hello\"}",
|
||||
"fresh": false,
|
||||
"hostname": "localhost",
|
||||
"ip": "",
|
||||
"ips": [],
|
||||
"protocol": "http",
|
||||
"query": {},
|
||||
"subdomains": [],
|
||||
"xhr": false,
|
||||
"os": {
|
||||
"hostname": "05512cb2102c"
|
||||
},
|
||||
"connection": {},
|
||||
"json": {
|
||||
"test": "hello"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertionResults": [
|
||||
{
|
||||
"uid": "hNBSF_GBdSTFHNiyCcOn9",
|
||||
"lhsExpr": "res.status",
|
||||
"rhsExpr": "200",
|
||||
"rhsOperand": "200",
|
||||
"operator": "eq",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"testResults": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.10.1",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"bru": "./bin/bru.js"
|
||||
@@ -20,13 +21,14 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.4.0",
|
||||
"@usebruno/lang": "0.3.0",
|
||||
"axios": "^1.3.2",
|
||||
"@usebruno/js": "0.6.0",
|
||||
"@usebruno/lang": "0.4.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"inquirer": "^9.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
|
||||
@@ -5,16 +5,21 @@ With Bruno CLI, you can now run your API collections with ease using simple comm
|
||||
This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Bruno CLI, use the node package manager of your choice, such as NPM:
|
||||
|
||||
```bash
|
||||
npm install -g @usebruno/cli
|
||||
```
|
||||
|
||||
## Getting started
|
||||
|
||||
Navigate to the directory where your API collection resides, and then run:
|
||||
|
||||
```bash
|
||||
bru run
|
||||
```
|
||||
|
||||
This command will run all the requests in your collection. You can also run a single request by specifying its filename:
|
||||
|
||||
```bash
|
||||
@@ -22,25 +27,37 @@ bru run request.bru
|
||||
```
|
||||
|
||||
Or run all requests in a collection's subfolder:
|
||||
|
||||
```bash
|
||||
bru run folder
|
||||
```
|
||||
|
||||
If you need to use an environment, you can specify it with the --env option:
|
||||
|
||||
```bash
|
||||
bru run folder --env Local
|
||||
```
|
||||
|
||||
If you need to collect the results of your API tests, you can specify the --output option:
|
||||
|
||||
```bash
|
||||
bru run folder --output results.json
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)
|
||||
|
||||
Thank you for using Bruno CLI!
|
||||
|
||||
## Changelog
|
||||
|
||||
See [here](packages/bruno-cli/changelog.md)
|
||||
|
||||
## License
|
||||
[MIT](license.md)
|
||||
|
||||
[MIT](license.md)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const { exists, isFile, isDirectory, getSubDirectories } = require('../utils/filesystem');
|
||||
const { forOwn } = require('lodash');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
@@ -33,7 +35,7 @@ const printRunSummary = (assertionResults, testResults) => {
|
||||
}
|
||||
testSummary += `, ${totalAssertions} total`;
|
||||
|
||||
console.log("\n" + chalk.bold(assertSummary));
|
||||
console.log('\n' + chalk.bold(assertSummary));
|
||||
console.log(chalk.bold(testSummary));
|
||||
|
||||
return {
|
||||
@@ -43,7 +45,7 @@ const printRunSummary = (assertionResults, testResults) => {
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getBruFilesRecursively = (dir) => {
|
||||
@@ -51,33 +53,34 @@ const getBruFilesRecursively = (dir) => {
|
||||
|
||||
const getFilesInOrder = (dir) => {
|
||||
let bruJsons = [];
|
||||
|
||||
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
|
||||
// todo: we might need a ignore config inside bruno.json
|
||||
if (stats.isDirectory() &&
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
filePath !== environmentsPath &&
|
||||
!filePath.startsWith(".git") &&
|
||||
!filePath.startsWith("node_modules")
|
||||
!filePath.startsWith('.git') &&
|
||||
!filePath.startsWith('node_modules')
|
||||
) {
|
||||
traverse(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const currentDirBruJsons = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
|
||||
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
@@ -97,13 +100,12 @@ const getBruFilesRecursively = (dir) => {
|
||||
|
||||
bruJsons = bruJsons.concat(currentDirBruJsons);
|
||||
};
|
||||
|
||||
|
||||
traverse(dir);
|
||||
return bruJsons;
|
||||
};
|
||||
|
||||
const bruJsons = getFilesInOrder(dir);
|
||||
return bruJsons;
|
||||
return getFilesInOrder(dir);
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
@@ -119,7 +121,16 @@ const builder = async (yargs) => {
|
||||
})
|
||||
.option('env', {
|
||||
describe: 'Environment variables',
|
||||
type: 'string',
|
||||
type: 'string'
|
||||
})
|
||||
.option('env-var', {
|
||||
describe: 'Overwrite a single environment variable, multiple usages possible',
|
||||
type: 'string'
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
describe: 'Path to write JSON results to',
|
||||
type: 'string'
|
||||
})
|
||||
.option('insecure', {
|
||||
type: 'boolean',
|
||||
@@ -129,17 +140,19 @@ const builder = async (yargs) => {
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
.example('$0 run folder -r', 'Run all requests in a folder recursively')
|
||||
.example(
|
||||
'$0 run request.bru --env local --env-var secret=xxx',
|
||||
'Run a request with the environment set to local and overwrite the variable secret with value xxx'
|
||||
)
|
||||
.example(
|
||||
'$0 run request.bru --output results.json',
|
||||
'Run a request and write the results to results.json in the current directory'
|
||||
);
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
try {
|
||||
let {
|
||||
filename,
|
||||
cacert,
|
||||
env,
|
||||
insecure,
|
||||
r: recursive
|
||||
} = argv;
|
||||
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
// todo
|
||||
@@ -147,30 +160,33 @@ const handler = async function (argv) {
|
||||
// will add support in the future to run it from anywhere inside the collection
|
||||
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
||||
const brunoJsonExists = await exists(brunoJsonPath);
|
||||
if(!brunoJsonExists) {
|
||||
if (!brunoJsonExists) {
|
||||
console.error(chalk.red(`You can run only at the root of a collection`));
|
||||
return;
|
||||
}
|
||||
|
||||
if(filename && filename.length) {
|
||||
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
|
||||
if (filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if(!pathExists) {
|
||||
if (!pathExists) {
|
||||
console.error(chalk.red(`File or directory ${filename} does not exist`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
filename = "./";
|
||||
filename = './';
|
||||
recursive = true;
|
||||
}
|
||||
|
||||
const collectionVariables = {};
|
||||
let envVars = {};
|
||||
|
||||
if(env) {
|
||||
if (env) {
|
||||
const envFile = path.join(collectionPath, 'environments', `${env}.bru`);
|
||||
const envPathExists = await exists(envFile);
|
||||
|
||||
if(!envPathExists) {
|
||||
if (!envPathExists) {
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`));
|
||||
return;
|
||||
}
|
||||
@@ -180,59 +196,90 @@ const handler = async function (argv) {
|
||||
envVars = getEnvVars(envJson);
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
if(insecure) {
|
||||
options['insecure'] = true
|
||||
}
|
||||
if(cacert && cacert.length) {
|
||||
if(insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
if (envVar) {
|
||||
let processVars;
|
||||
if (typeof envVar === 'string') {
|
||||
processVars = [envVar];
|
||||
} else if (typeof envVar === 'object' && Array.isArray(envVar)) {
|
||||
processVars = envVar;
|
||||
} else {
|
||||
console.error(chalk.red(`overridable environment variables not parsable: use name=value`));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
const pathExists = await exists(cacert);
|
||||
if(pathExists) {
|
||||
options['cacert'] = cacert
|
||||
if (processVars && Array.isArray(processVars)) {
|
||||
for (const value of processVars.values()) {
|
||||
// split the string at the first equals sign
|
||||
const match = value.match(/^([^=]+)=(.*)$/);
|
||||
if (!match) {
|
||||
console.error(
|
||||
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
|
||||
chalk.dim(`${value}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
envVars[match[1]] = match[2];
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
if (insecure) {
|
||||
options['insecure'] = true;
|
||||
}
|
||||
if (cacert && cacert.length) {
|
||||
if (insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
} else {
|
||||
const pathExists = await exists(cacert);
|
||||
if (pathExists) {
|
||||
options['cacert'] = cacert;
|
||||
} else {
|
||||
console.error(chalk.red(`Cacert File ${cacert} does not exist`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load .env file at root of collection if it exists
|
||||
const dotEnvPath = path.join(collectionPath, '.env');
|
||||
const dotEnvExists = await exists(dotEnvPath);
|
||||
const processEnvVars = {
|
||||
...process.env
|
||||
};
|
||||
if (dotEnvExists) {
|
||||
const content = fs.readFileSync(dotEnvPath, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
|
||||
forOwn(jsonData, (value, key) => {
|
||||
processEnvVars[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const _isFile = await isFile(filename);
|
||||
if(_isFile) {
|
||||
let assertionResults = [];
|
||||
let testResults = [];
|
||||
let testrunResults = [];
|
||||
|
||||
let bruJsons = [];
|
||||
|
||||
if (_isFile) {
|
||||
console.log(chalk.yellow('Running Request \n'));
|
||||
const bruContent = fs.readFileSync(filename, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
const result = await runSingleRequest(filename, bruJson, collectionPath, collectionVariables, envVars);
|
||||
|
||||
if(result) {
|
||||
const {
|
||||
assertionResults,
|
||||
testResults
|
||||
} = result;
|
||||
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Done.')));
|
||||
|
||||
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
bruJsons.push({
|
||||
bruFilepath: filename,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
|
||||
const _isDirectory = await isDirectory(filename);
|
||||
if(_isDirectory) {
|
||||
let bruJsons = [];
|
||||
if(!recursive) {
|
||||
if (_isDirectory) {
|
||||
if (!recursive) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
const files = fs.readdirSync(filename);
|
||||
const bruFiles = files.filter((file) => file.endsWith('.bru'));
|
||||
|
||||
for (const bruFile of bruFiles) {
|
||||
const bruFilepath = path.join(filename, bruFile)
|
||||
const bruFilepath = path.join(filename, bruFile);
|
||||
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
bruJsons.push({
|
||||
@@ -240,8 +287,6 @@ const handler = async function (argv) {
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
|
||||
// order requests by sequence
|
||||
bruJsons.sort((a, b) => {
|
||||
const aSequence = a.bruJson.seq || 0;
|
||||
const bSequence = b.bruJson.seq || 0;
|
||||
@@ -250,47 +295,64 @@ const handler = async function (argv) {
|
||||
} else {
|
||||
console.log(chalk.yellow('Running Folder Recursively \n'));
|
||||
|
||||
bruJsons = await getBruFilesRecursively(filename);
|
||||
}
|
||||
|
||||
let assertionResults = [];
|
||||
let testResults = [];
|
||||
|
||||
for (const iter of bruJsons) {
|
||||
const {
|
||||
bruFilepath,
|
||||
bruJson
|
||||
} = iter;
|
||||
const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars);
|
||||
|
||||
if(result) {
|
||||
const {
|
||||
assertionResults: _assertionResults,
|
||||
testResults: _testResults
|
||||
} = result;
|
||||
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
}
|
||||
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
|
||||
if(summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
process.exit(1);
|
||||
bruJsons = getBruFilesRecursively(filename);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iter of bruJsons) {
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
const result = await runSingleRequest(
|
||||
bruFilepath,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig
|
||||
);
|
||||
|
||||
if (result) {
|
||||
testrunResults.push(result);
|
||||
const { assertionResults: _assertionResults, testResults: _testResults } = result;
|
||||
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
}
|
||||
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
|
||||
if (outputPath && outputPath.length) {
|
||||
const outputDir = path.dirname(outputPath);
|
||||
const outputDirExists = await exists(outputDir);
|
||||
if (!outputDirExists) {
|
||||
console.error(chalk.red(`Output directory ${outputDir} does not exist`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputJson = {
|
||||
summary,
|
||||
results: testrunResults
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
|
||||
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
|
||||
}
|
||||
|
||||
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Something went wrong");
|
||||
console.log('Something went wrong');
|
||||
console.error(chalk.red(err.message));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
command,
|
||||
desc,
|
||||
builder,
|
||||
command,
|
||||
desc,
|
||||
builder,
|
||||
handler
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const { CLI_EPILOGUE, CLI_VERSION } = require('./constants');
|
||||
|
||||
const printBanner = () => {
|
||||
console.log(chalk.yellow(`Bru CLI ${CLI_VERSION}`));
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const argLength = process.argv.length;
|
||||
@@ -20,7 +20,7 @@ const run = async () => {
|
||||
.commandDir('commands')
|
||||
.epilogue(CLI_EPILOGUE)
|
||||
.usage('Usage: $0 <command> [options]')
|
||||
.demandCommand(1, "Woof !! Let's play with some apis !!")
|
||||
.demandCommand(1, "Woof !! Let's play with some APIs !!")
|
||||
.help('h')
|
||||
.alias('h', 'help');
|
||||
};
|
||||
|
||||
@@ -1,55 +1,94 @@
|
||||
const Mustache = require('mustache');
|
||||
const { each, forOwn } = require('lodash');
|
||||
const Handlebars = require('handlebars');
|
||||
const { each, forOwn, cloneDeep } = require('lodash');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
forOwn(headers, (value, key) => {
|
||||
if (key && key.toLowerCase() === 'content-type') {
|
||||
contentType = value;
|
||||
}
|
||||
});
|
||||
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
|
||||
const interpolateEnvVars = (str, processEnvVars) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
return template({
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
|
||||
// envVars can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVars first with processEnvVars
|
||||
forOwn(envVars, (value, key) => {
|
||||
envVars[key] = interpolateEnvVars(value, processEnvVars);
|
||||
});
|
||||
|
||||
const interpolate = (str) => {
|
||||
if(!str || !str.length || typeof str !== "string") {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
// collectionVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables
|
||||
...collectionVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Mustache.render(str, combinedVars);
|
||||
return template(combinedVars);
|
||||
};
|
||||
|
||||
request.url = interpolate(request.url);
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
request.headers[key] = interpolate(value);
|
||||
delete request.headers[key];
|
||||
request.headers[interpolate(key)] = interpolate(value);
|
||||
});
|
||||
|
||||
if(request.headers["content-type"] === "application/json") {
|
||||
if(typeof request.data === "object") {
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
if(typeof request.data === "string") {
|
||||
if(request.data.length) {
|
||||
if (typeof request.data === 'string') {
|
||||
if (request.data.length) {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if(request.headers["content-type"] === "application/x-www-form-urlencoded") {
|
||||
if(typeof request.data === "object") {
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
} else {
|
||||
request.data = interpolate(request.data);
|
||||
@@ -59,6 +98,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
|
||||
param.value = interpolate(param.value);
|
||||
});
|
||||
|
||||
if (request.proxy) {
|
||||
request.proxy.protocol = interpolate(request.proxy.protocol);
|
||||
request.proxy.hostname = interpolate(request.proxy.hostname);
|
||||
request.proxy.port = interpolate(request.proxy.port);
|
||||
|
||||
if (request.proxy.auth) {
|
||||
request.proxy.auth.username = interpolate(request.proxy.auth.username);
|
||||
request.proxy.auth.password = interpolate(request.proxy.auth.password);
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,15 @@ const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@use
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
|
||||
const runSingleRequest = async function (filename, bruJson, collectionPath, collectionVariables, envVariables) {
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
envVariables,
|
||||
processEnvVars,
|
||||
brunoConfig
|
||||
) {
|
||||
let request;
|
||||
|
||||
try {
|
||||
@@ -19,7 +27,7 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
|
||||
// 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') {
|
||||
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
const form = new FormData();
|
||||
forOwn(request.data, (value, key) => {
|
||||
form.append(key, value);
|
||||
@@ -30,48 +38,87 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if(preRequestVars && preRequestVars.length) {
|
||||
if (preRequestVars && preRequestVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPreRequestVars(preRequestVars, request, envVariables, collectionVariables, collectionPath);
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(bruJson, 'request.script.req');
|
||||
if(requestScriptFile && requestScriptFile.length) {
|
||||
if (requestScriptFile && requestScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath);
|
||||
await scriptRuntime.runRequestScript(
|
||||
requestScriptFile,
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// set proxy if enabled
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
|
||||
const proxyHostname = get(brunoConfig, 'proxy.hostname');
|
||||
const proxyPort = get(brunoConfig, 'proxy.port');
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
|
||||
const proxyConfig = {
|
||||
protocol: proxyProtocol,
|
||||
hostname: proxyHostname,
|
||||
port: proxyPort
|
||||
};
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
|
||||
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
|
||||
proxyConfig.auth = {
|
||||
username: proxyAuthUsername,
|
||||
password: proxyAuthPassword
|
||||
};
|
||||
}
|
||||
|
||||
request.proxy = proxyConfig;
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables);
|
||||
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const httpsAgentRequestFields = {};
|
||||
if(insecure) {
|
||||
if (insecure) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||
const cacert = cacertArray.find(el => el);
|
||||
if(cacert && cacert.length > 1) {
|
||||
const cacert = cacertArray.find((el) => el);
|
||||
if (cacert && cacert.length > 1) {
|
||||
try {
|
||||
caCrt = fs.readFileSync(cacert);
|
||||
httpsAgentRequestFields['ca'] = caCrt;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
console.log('Error reading CA cert file:' + cacert, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
// stringify the request url encoded params
|
||||
if(request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
request.data = qs.stringify(request.data);
|
||||
}
|
||||
|
||||
@@ -82,27 +129,51 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(postResponseVars, request, response, envVariables, collectionVariables, collectionPath);
|
||||
varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if(responseScriptFile && responseScriptFile.length) {
|
||||
if (responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
await scriptRuntime.runResponseScript(
|
||||
responseScriptFile,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
if (assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath);
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if(r.status === 'pass') {
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
@@ -114,15 +185,24 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
if (testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, response, envVariables, collectionVariables, collectionPath);
|
||||
const result = await testRuntime.runTests(
|
||||
testFile,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
if(testResults && testResults.length) {
|
||||
if (testResults && testResults.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if(testResult.status === 'pass') {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
@@ -131,36 +211,74 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
}
|
||||
|
||||
return {
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
},
|
||||
assertionResults,
|
||||
testResults
|
||||
};
|
||||
} catch (err) {
|
||||
if(err && err.response) {
|
||||
console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`));
|
||||
if (err && err.response) {
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if(postResponseVars && postResponseVars.length) {
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(postResponseVars, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run post response script
|
||||
const responseScriptFile = get(bruJson, 'request.script.res');
|
||||
if(responseScriptFile && responseScriptFile.length) {
|
||||
if (responseScriptFile && responseScriptFile.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(responseScriptFile, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
await scriptRuntime.runResponseScript(
|
||||
responseScriptFile,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if(assertions && assertions.length) {
|
||||
if (assertions && assertions.length) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(assertions, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if(r.status === 'pass') {
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
@@ -172,15 +290,24 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if(testFile && testFile.length) {
|
||||
if (testFile && testFile.length) {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = testRuntime.runTests(testFile, request, err.response, envVariables, collectionVariables, collectionPath);
|
||||
const result = await testRuntime.runTests(
|
||||
testFile,
|
||||
request,
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
if(testResults && testResults.length) {
|
||||
if (testResults && testResults.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if(testResult.status === 'pass') {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
|
||||
@@ -9,10 +9,10 @@ Mustache.escape = function (value) {
|
||||
|
||||
/**
|
||||
* The transformer function for converting a BRU file to JSON.
|
||||
*
|
||||
*
|
||||
* We map the json response from the bru lang and transform it into the DSL
|
||||
* format that is used by the bruno app
|
||||
*
|
||||
*
|
||||
* @param {string} bru The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
@@ -20,35 +20,35 @@ const bruToJson = (bru) => {
|
||||
try {
|
||||
const json = bruToJsonV2(bru);
|
||||
|
||||
let requestType = _.get(json, "meta.type");
|
||||
if(requestType === "http") {
|
||||
requestType = "http-request"
|
||||
} else if(requestType === "graphql") {
|
||||
requestType = "graphql-request";
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
requestType = 'http-request';
|
||||
} else if (requestType === 'graphql') {
|
||||
requestType = 'graphql-request';
|
||||
} else {
|
||||
requestType = "http";
|
||||
requestType = 'http';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, "meta.seq")
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
|
||||
const transformedJson = {
|
||||
"type": requestType,
|
||||
"name": _.get(json, "meta.name"),
|
||||
"seq": !isNaN(sequence) ? Number(sequence) : 1,
|
||||
"request": {
|
||||
"method": _.upperCase(_.get(json, "http.method")),
|
||||
"url": _.get(json, "http.url"),
|
||||
"params": _.get(json, "query", []),
|
||||
"headers": _.get(json, "headers", []),
|
||||
"body": _.get(json, "body", {}),
|
||||
"vars": _.get(json, "vars", []),
|
||||
"assertions": _.get(json, "assertions", []),
|
||||
"script": _.get(json, "script", ""),
|
||||
"tests": _.get(json, "tests", "")
|
||||
type: requestType,
|
||||
name: _.get(json, 'meta.name'),
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1,
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
body: _.get(json, 'body', {}),
|
||||
vars: _.get(json, 'vars', []),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
script: _.get(json, 'script', ''),
|
||||
tests: _.get(json, 'tests', '')
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.body.mode = _.get(json, "http.body", "none");
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
} catch (err) {
|
||||
@@ -72,7 +72,7 @@ const getEnvVars = (environment = {}) => {
|
||||
|
||||
const envVars = {};
|
||||
_.each(variables, (variable) => {
|
||||
if(variable.enabled) {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = Mustache.escape(variable.value);
|
||||
}
|
||||
});
|
||||
@@ -83,7 +83,7 @@ const getEnvVars = (environment = {}) => {
|
||||
const options = {};
|
||||
const getOptions = () => {
|
||||
return options;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
|
||||
@@ -12,7 +12,7 @@ const rpad = (str, width) => {
|
||||
paddedStr = paddedStr + ' ';
|
||||
}
|
||||
return paddedStr;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
lpad,
|
||||
|
||||
@@ -2,7 +2,7 @@ const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const fsPromises = require('fs/promises');
|
||||
|
||||
const exists = async p => {
|
||||
const exists = async (p) => {
|
||||
try {
|
||||
await fsPromises.access(p);
|
||||
return true;
|
||||
@@ -11,7 +11,7 @@ const exists = async p => {
|
||||
}
|
||||
};
|
||||
|
||||
const isSymbolicLink = filepath => {
|
||||
const isSymbolicLink = (filepath) => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();
|
||||
} catch (_) {
|
||||
@@ -19,7 +19,7 @@ const isSymbolicLink = filepath => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFile = filepath => {
|
||||
const isFile = (filepath) => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
|
||||
} catch (_) {
|
||||
@@ -27,7 +27,7 @@ const isFile = filepath => {
|
||||
}
|
||||
};
|
||||
|
||||
const isDirectory = dirPath => {
|
||||
const isDirectory = (dirPath) => {
|
||||
try {
|
||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||
} catch (_) {
|
||||
@@ -35,14 +35,14 @@ const isDirectory = dirPath => {
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeAndResolvePath = pathname => {
|
||||
const normalizeAndResolvePath = (pathname) => {
|
||||
if (isSymbolicLink(pathname)) {
|
||||
const absPath = path.dirname(pathname);
|
||||
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
|
||||
if (isFile(targetPath) || isDirectory(targetPath)) {
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
|
||||
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`);
|
||||
return '';
|
||||
}
|
||||
return path.resolve(pathname);
|
||||
@@ -51,29 +51,29 @@ const normalizeAndResolvePath = pathname => {
|
||||
const writeFile = async (pathname, content) => {
|
||||
try {
|
||||
fs.writeFileSync(pathname, content, {
|
||||
encoding: "utf8"
|
||||
encoding: 'utf8'
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const hasJsonExtension = filename => {
|
||||
if (!filename || typeof filename !== 'string') return false
|
||||
return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
|
||||
}
|
||||
const hasJsonExtension = (filename) => {
|
||||
if (!filename || typeof filename !== 'string') return false;
|
||||
return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
|
||||
};
|
||||
|
||||
const hasBruExtension = filename => {
|
||||
if (!filename || typeof filename !== 'string') return false
|
||||
return ['bru'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
|
||||
}
|
||||
const hasBruExtension = (filename) => {
|
||||
if (!filename || typeof filename !== 'string') return false;
|
||||
return ['bru'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
|
||||
};
|
||||
|
||||
const createDirectory = async (dir) => {
|
||||
if(!dir) {
|
||||
if (!dir) {
|
||||
throw new Error(`directory: path is null`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(dir)){
|
||||
if (fs.existsSync(dir)) {
|
||||
throw new Error(`directory: ${dir} already exists`);
|
||||
}
|
||||
|
||||
@@ -93,15 +93,15 @@ const searchForFiles = (dir, extension) => {
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
const searchForBruFiles = (dir) => {
|
||||
return searchForFiles(dir, '.bru');
|
||||
};
|
||||
|
||||
const stripExtension = (filename = '') => {
|
||||
return filename.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
return filename.replace(/\.[^/.]+$/, '');
|
||||
};
|
||||
|
||||
const getSubDirectories = (dir) => {
|
||||
try {
|
||||
@@ -112,7 +112,7 @@ const getSubDirectories = (dir) => {
|
||||
})
|
||||
.sort();
|
||||
|
||||
return subDirectories;
|
||||
return subDirectories;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "v0.14.0",
|
||||
"version": "v0.16.6",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
"private": true,
|
||||
"main": "src/index.js",
|
||||
@@ -10,13 +10,15 @@
|
||||
"clean": "rimraf dist",
|
||||
"dev": "electron .",
|
||||
"dist": "electron-builder --mac --config electron-builder-config.js",
|
||||
"pack": "electron-builder --dir"
|
||||
"pack": "electron-builder --dir",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.4.0",
|
||||
"@usebruno/lang": "0.3.0",
|
||||
"@usebruno/schema": "0.3.1",
|
||||
"axios": "^0.26.0",
|
||||
"@usebruno/js": "0.6.0",
|
||||
"@usebruno/lang": "0.4.0",
|
||||
"@usebruno/schema": "0.5.0",
|
||||
"about-window": "^1.15.2",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"dotenv": "^16.0.3",
|
||||
@@ -27,10 +29,12 @@
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"qs": "^6.11.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vm2": "^3.9.13",
|
||||
|
||||
BIN
packages/bruno-electron/src/about/256x256.png
Normal file
BIN
packages/bruno-electron/src/about/256x256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
8
packages/bruno-electron/src/about/about.css
Normal file
8
packages/bruno-electron/src/about/about.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.versions {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.title {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
@@ -5,39 +5,33 @@ const Yup = require('yup');
|
||||
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
|
||||
// uid inside collections is deprecated, but we still need to validate it
|
||||
// for backward compatibility
|
||||
const uidSchema = Yup.string()
|
||||
.length(21, 'uid must be 21 characters in length')
|
||||
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric');
|
||||
|
||||
// todo: bruno.json config schema validation errors must be propagated to the UI
|
||||
const configSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
name: Yup.string().nullable().max(256, 'name must be 256 characters or less'),
|
||||
name: Yup.string().max(256, 'name must be 256 characters or less').required('name is required'),
|
||||
type: Yup.string().oneOf(['collection']).required('type is required'),
|
||||
version: Yup.string().oneOf(['1']).required('type is required')
|
||||
}).noUnknown(true).strict();
|
||||
});
|
||||
|
||||
const readConfigFile = async (pathname) => {
|
||||
try {
|
||||
const jsonData = fs.readFileSync(pathname, 'utf8');
|
||||
return JSON.parse(jsonData);
|
||||
} catch(err) {
|
||||
return Promise.reject(new Error("Unable to parse json in bruno.json"));
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('Unable to parse json in bruno.json'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateSchema = async (config) => {
|
||||
try {
|
||||
await configSchema.validate(config);
|
||||
} catch(err) {
|
||||
return Promise.reject(new Error("bruno.json format is invalid"));
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('bruno.json format is invalid'));
|
||||
}
|
||||
};
|
||||
|
||||
const getCollectionConfigFile = async (pathname) => {
|
||||
const configFilePath = path.join(pathname, 'bruno.json');
|
||||
if (!fs.existsSync(configFilePath)){
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
throw new Error(`The collection is not valid (bruno.json not found)`);
|
||||
}
|
||||
|
||||
@@ -45,7 +39,7 @@ const getCollectionConfigFile = async (pathname) => {
|
||||
await validateSchema(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
const openCollectionDialog = async (win, watcher) => {
|
||||
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||
@@ -60,22 +54,20 @@ const openCollectionDialog = async (win, watcher) => {
|
||||
console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openCollection = async (win, watcher, collectionPath, options = {}) => {
|
||||
if(!watcher.hasWatcher(collectionPath)) {
|
||||
if (!watcher.hasWatcher(collectionPath)) {
|
||||
try {
|
||||
const {
|
||||
name
|
||||
} = await getCollectionConfigFile(collectionPath);
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
const brunoConfig = await getCollectionConfigFile(collectionPath);
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
|
||||
win.webContents.send('main:collection-opened', collectionPath, uid, name);
|
||||
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', win, collectionPath, uid);
|
||||
} catch(err) {
|
||||
if(!options.dontSendDisplayErrors) {
|
||||
} catch (err) {
|
||||
if (!options.dontSendDisplayErrors) {
|
||||
win.webContents.send('main:display-error', {
|
||||
error: err.message || 'An error occured while opening the local collection'
|
||||
error: err.message || 'An error occurred while opening the local collection'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const { ipcMain } = require('electron');
|
||||
const openAboutWindow = require('about-window').default;
|
||||
const { join } = require('path');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'Collection',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Local Collection',
|
||||
click () {
|
||||
label: 'Open Collection',
|
||||
click() {
|
||||
ipcMain.emit('main:open-collection');
|
||||
}
|
||||
},
|
||||
@@ -14,39 +16,48 @@ const template = [
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo'},
|
||||
{ role: 'redo'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'cut'},
|
||||
{ role: 'copy'},
|
||||
{ role: 'paste'}
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'toggledevtools'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'resetzoom'},
|
||||
{ role: 'zoomin'},
|
||||
{ role: 'zoomout'},
|
||||
{ type: 'separator'},
|
||||
{ role: 'togglefullscreen'}
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'toggledevtools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetzoom' },
|
||||
{ role: 'zoomin' },
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{ role: 'minimize'},
|
||||
{ role: 'close'}
|
||||
]
|
||||
role: 'window',
|
||||
submenu: [{ role: 'minimize' }, { role: 'close' }]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{ label: 'Learn More'}
|
||||
{
|
||||
label: 'About Bruno',
|
||||
click: () =>
|
||||
openAboutWindow({
|
||||
product_name: 'Bruno',
|
||||
icon_path: join(__dirname, '../about/256x256.png'),
|
||||
css_path: join(__dirname, '../about/about.css'),
|
||||
homepage: 'https://www.usebruno.com/',
|
||||
package_json_dir: join(__dirname, '../..')
|
||||
})
|
||||
},
|
||||
{ label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3,22 +3,19 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
|
||||
const {
|
||||
bruToEnvJson,
|
||||
envJsonToBru,
|
||||
bruToJson,
|
||||
jsonToBru
|
||||
} = require('../bru');
|
||||
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
const {
|
||||
isLegacyEnvFile,
|
||||
migrateLegacyEnvFile,
|
||||
isLegacyBruFile,
|
||||
migrateLegacyBruFile
|
||||
} = require('../bru/migrate');
|
||||
const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
|
||||
const { itemSchema } = require('@usebruno/schema');
|
||||
const { uuid } = require('../utils/common');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { decryptString } = require('../utils/encryption');
|
||||
const { setDotEnvVars } = require('../store/process-env');
|
||||
const { setBrunoConfig } = require('../store/bruno-config');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
@@ -27,6 +24,20 @@ const isJsonEnvironmentConfig = (pathname, collectionPath) => {
|
||||
return dirname === collectionPath && basename === 'environments.json';
|
||||
};
|
||||
|
||||
const isDotEnvFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return dirname === collectionPath && basename === '.env';
|
||||
};
|
||||
|
||||
const isBrunoConfigFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return dirname === collectionPath && basename === 'bruno.json';
|
||||
};
|
||||
|
||||
const isBruEnvironmentConfig = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
@@ -46,18 +57,24 @@ const hydrateRequestWithUuid = (request, pathname) => {
|
||||
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
|
||||
|
||||
params.forEach((param) => param.uid = uuid());
|
||||
headers.forEach((header) => header.uid = uuid());
|
||||
requestVars.forEach((variable) => variable.uid = uuid());
|
||||
responseVars.forEach((variable) => variable.uid = uuid());
|
||||
assertions.forEach((assertion) => assertion.uid = uuid());
|
||||
bodyFormUrlEncoded.forEach((param) => param.uid = uuid());
|
||||
bodyMultipartForm.forEach((param) => param.uid = uuid());
|
||||
params.forEach((param) => (param.uid = uuid()));
|
||||
headers.forEach((header) => (header.uid = uuid()));
|
||||
requestVars.forEach((variable) => (variable.uid = uuid()));
|
||||
responseVars.forEach((variable) => (variable.uid = uuid()));
|
||||
assertions.forEach((assertion) => (assertion.uid = uuid()));
|
||||
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
|
||||
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
|
||||
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
const addEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
const envHasSecrets = (environment = {}) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||
|
||||
return secrets && secrets.length > 0;
|
||||
};
|
||||
|
||||
const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
const basename = path.basename(pathname);
|
||||
const file = {
|
||||
@@ -65,13 +82,13 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: basename
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
// migrate old env json to bru file
|
||||
if(isLegacyEnvFile(bruContent)) {
|
||||
if (isLegacyEnvFile(bruContent)) {
|
||||
bruContent = await migrateLegacyEnvFile(bruContent, pathname);
|
||||
}
|
||||
|
||||
@@ -79,14 +96,26 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
|
||||
|
||||
// hydrate environment variables with secrets
|
||||
if (envHasSecrets(file.data)) {
|
||||
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable) {
|
||||
variable.value = decryptString(secret.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const changeEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
const basename = path.basename(pathname);
|
||||
const file = {
|
||||
@@ -101,14 +130,25 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
file.data = bruToEnvJson(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
|
||||
|
||||
// hydrate environment variables with secrets
|
||||
if (envHasSecrets(file.data)) {
|
||||
const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable) {
|
||||
variable.value = decryptString(secret.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// we are reusing the addEnvironmentFile event itself
|
||||
// this is because the uid of the pathname remains the same
|
||||
// and the collection tree will be able to update the existing environment
|
||||
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -118,24 +158,54 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
name: path.basename(pathname)
|
||||
},
|
||||
data: {
|
||||
uid: getRequestUid(pathname),
|
||||
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
|
||||
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4)
|
||||
}
|
||||
};
|
||||
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkEnvironmentFile', file);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
console.log(`watcher add: ${pathname}`);
|
||||
|
||||
if(isJsonEnvironmentConfig(pathname, collectionPath)) {
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const brunoConfig = JSON.parse(content);
|
||||
|
||||
setBrunoConfig(collectionUid, brunoConfig);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDotEnvFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
|
||||
setDotEnvVars(collectionUid, jsonData);
|
||||
const payload = {
|
||||
collectionUid,
|
||||
processEnvVariables: {
|
||||
...process.env,
|
||||
...jsonData
|
||||
}
|
||||
};
|
||||
win.webContents.send('main:process-env-update', payload);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isJsonEnvironmentConfig(pathname, collectionPath)) {
|
||||
try {
|
||||
const dirname = path.dirname(pathname);
|
||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
@@ -147,7 +217,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
fs.mkdirSync(envDirectory);
|
||||
}
|
||||
|
||||
for(const env of jsonData) {
|
||||
for (const env of jsonData) {
|
||||
const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
|
||||
const bruContent = envJsonToBru(env);
|
||||
await writeFile(bruEnvFilename, bruContent);
|
||||
@@ -161,12 +231,12 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return addEnvironmentFile(win, pathname, collectionUid);
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
|
||||
}
|
||||
|
||||
// migrate old json files to bru
|
||||
if(hasJsonExtension(pathname)) {
|
||||
if (hasJsonExtension(pathname)) {
|
||||
try {
|
||||
const json = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = JSON.parse(json);
|
||||
@@ -178,7 +248,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const re = /(.*)\.json$/;
|
||||
const subst = `$1.bru`;
|
||||
const bruFilename = pathname.replace(re, subst);
|
||||
|
||||
|
||||
await writeFile(bruFilename, content);
|
||||
await fs.unlinkSync(pathname);
|
||||
} catch (err) {
|
||||
@@ -186,20 +256,20 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
}
|
||||
|
||||
if(hasBruExtension(pathname)) {
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
// migrate old bru format to new bru format
|
||||
if(isLegacyBruFile(bruContent)) {
|
||||
if (isLegacyBruFile(bruContent)) {
|
||||
bruContent = await migrateLegacyBruFile(bruContent, pathname);
|
||||
}
|
||||
|
||||
@@ -207,7 +277,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -215,7 +285,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if(pathname === envDirectory) {
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -223,44 +293,79 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
|
||||
};
|
||||
|
||||
const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
if(isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return changeEnvironmentFile(win, pathname, collectionUid);
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const brunoConfig = JSON.parse(content);
|
||||
|
||||
const payload = {
|
||||
collectionUid,
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
setBrunoConfig(collectionUid, brunoConfig);
|
||||
win.webContents.send('main:bruno-config-update', payload);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if(hasBruExtension(pathname)) {
|
||||
if (isDotEnvFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
|
||||
setDotEnvVars(collectionUid, jsonData);
|
||||
const payload = {
|
||||
collectionUid,
|
||||
processEnvVariables: {
|
||||
...process.env,
|
||||
...jsonData
|
||||
}
|
||||
};
|
||||
win.webContents.send('main:process-env-update', payload);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);
|
||||
}
|
||||
|
||||
if (hasBruExtension(pathname)) {
|
||||
try {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const bru = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = bruToJson(bru);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
if(isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
return unlinkEnvironmentFile(win, pathname, collectionUid);
|
||||
}
|
||||
|
||||
if(hasBruExtension(pathname)) {
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
@@ -270,12 +375,12 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
};
|
||||
win.webContents.send('main:collection-tree-updated', 'unlink', file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if(pathname === envDirectory) {
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,15 +392,15 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||
}
|
||||
};
|
||||
|
||||
class Watcher {
|
||||
constructor () {
|
||||
constructor() {
|
||||
this.watchers = {};
|
||||
}
|
||||
|
||||
addWatcher (win, watchPath, collectionUid) {
|
||||
if(this.watchers[watchPath]) {
|
||||
addWatcher(win, watchPath, collectionUid) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
|
||||
@@ -309,7 +414,7 @@ class Watcher {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
ignoreInitial: false,
|
||||
usePolling: false,
|
||||
ignored: path => ["node_modules", ".git", "bruno.json"].some(s => path.includes(s)),
|
||||
ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
|
||||
persistent: true,
|
||||
ignorePermissionErrors: true,
|
||||
awaitWriteFinish: {
|
||||
@@ -318,28 +423,28 @@ class Watcher {
|
||||
},
|
||||
depth: 20
|
||||
});
|
||||
|
||||
|
||||
watcher
|
||||
.on('add', pathname => add(win, pathname, collectionUid, watchPath))
|
||||
.on('addDir', pathname => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', pathname => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', pathname => unlink(win, pathname, collectionUid, watchPath))
|
||||
.on('unlinkDir', pathname => unlinkDir(win, pathname, collectionUid, watchPath))
|
||||
|
||||
self.watchers[watchPath] = watcher;
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath));
|
||||
|
||||
self.watchers[watchPath] = watcher;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hasWatcher (watchPath) {
|
||||
hasWatcher(watchPath) {
|
||||
return this.watchers[watchPath];
|
||||
}
|
||||
|
||||
removeWatcher (watchPath, win) {
|
||||
if(this.watchers[watchPath]) {
|
||||
removeWatcher(watchPath, win) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
this.watchers[watchPath] = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Watcher;
|
||||
module.exports = Watcher;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
bruToJsonV2,
|
||||
jsonToBruV2,
|
||||
bruToEnvJsonV2,
|
||||
envJsonToBruV2
|
||||
} = require('@usebruno/lang');
|
||||
const { bruToJsonV2, jsonToBruV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
|
||||
const { each } = require('lodash');
|
||||
|
||||
const bruToEnvJson = (bru) => {
|
||||
@@ -12,33 +7,33 @@ const bruToEnvJson = (bru) => {
|
||||
const json = bruToEnvJsonV2(bru);
|
||||
|
||||
// the app env format requires each variable to have a type
|
||||
// this need to be evaulated and safely removed
|
||||
// this need to be evaluated and safely removed
|
||||
// i don't see it being used in schema validation
|
||||
if(json && json.variables && json.variables.length) {
|
||||
each(json.variables, (v) => v.type = "text");
|
||||
if (json && json.variables && json.variables.length) {
|
||||
each(json.variables, (v) => (v.type = 'text'));
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (error) {
|
||||
return Promise.reject(e);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const envJsonToBru = (json) => {
|
||||
try {
|
||||
const bru = envJsonToBruV2(json);
|
||||
return bru;
|
||||
} catch (error) {
|
||||
return Promise.reject(e);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a BRU file to JSON.
|
||||
*
|
||||
*
|
||||
* We map the json response from the bru lang and transform it into the DSL
|
||||
* format that the app users
|
||||
*
|
||||
* format that the app uses
|
||||
*
|
||||
* @param {string} bru The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
@@ -46,35 +41,35 @@ const bruToJson = (bru) => {
|
||||
try {
|
||||
const json = bruToJsonV2(bru);
|
||||
|
||||
let requestType = _.get(json, "meta.type");
|
||||
if(requestType === "http") {
|
||||
requestType = "http-request"
|
||||
} else if(requestType === "graphql") {
|
||||
requestType = "graphql-request";
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
requestType = 'http-request';
|
||||
} else if (requestType === 'graphql') {
|
||||
requestType = 'graphql-request';
|
||||
} else {
|
||||
requestType = "http-request";
|
||||
requestType = 'http-request';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, "meta.seq")
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
|
||||
const transformedJson = {
|
||||
"type": requestType,
|
||||
"name": _.get(json, "meta.name"),
|
||||
"seq": !isNaN(sequence) ? Number(sequence) : 1,
|
||||
"request": {
|
||||
"method": _.upperCase(_.get(json, "http.method")),
|
||||
"url": _.get(json, "http.url"),
|
||||
"params": _.get(json, "query", []),
|
||||
"headers": _.get(json, "headers", []),
|
||||
"body": _.get(json, "body", {}),
|
||||
"script": _.get(json, "script", {}),
|
||||
"vars": _.get(json, "vars", {}),
|
||||
"assertions": _.get(json, "assertions", []),
|
||||
"tests": _.get(json, "tests", "")
|
||||
type: requestType,
|
||||
name: _.get(json, 'meta.name'),
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1,
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
tests: _.get(json, 'tests', '')
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.body.mode = _.get(json, "http.body", "none");
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
} catch (e) {
|
||||
@@ -83,28 +78,28 @@ const bruToJson = (bru) => {
|
||||
};
|
||||
/**
|
||||
* The transformer function for converting a JSON to BRU file.
|
||||
*
|
||||
*
|
||||
* We map the json response from the app and transform it into the DSL
|
||||
* format that the bru lang understands
|
||||
*
|
||||
*
|
||||
* @param {object} json The JSON representation of the BRU file.
|
||||
* @returns {string} The BRU file content.
|
||||
*/
|
||||
const jsonToBru = (json) => {
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = "http";
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = "graphql";
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = "http";
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: _.get(json, 'seq'),
|
||||
seq: _.get(json, 'seq')
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
@@ -120,7 +115,7 @@ const jsonToBru = (json) => {
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
tests: _.get(json, 'request.tests', '')
|
||||
};
|
||||
|
||||
return jsonToBruV2(bruJson);
|
||||
@@ -130,5 +125,5 @@ module.exports = {
|
||||
bruToJson,
|
||||
jsonToBru,
|
||||
bruToEnvJson,
|
||||
envJsonToBru,
|
||||
envJsonToBru
|
||||
};
|
||||
|
||||
@@ -34,15 +34,15 @@ const isLegacyBruFile = (bruContent = '') => {
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (line.startsWith("name")) {
|
||||
if (line.startsWith('name')) {
|
||||
hasName = true;
|
||||
} else if (line.startsWith("method")) {
|
||||
} else if (line.startsWith('method')) {
|
||||
hasMethod = true;
|
||||
} else if (line.startsWith("url")) {
|
||||
} else if (line.startsWith('url')) {
|
||||
hasUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return hasName && hasMethod && hasUrl;
|
||||
};
|
||||
|
||||
@@ -51,16 +51,16 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
|
||||
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = "http";
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = "graphql";
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = "http";
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
let script = {};
|
||||
let legacyScript = _.get(json, 'request.script');
|
||||
if(legacyScript && legacyScript.trim().length > 0) {
|
||||
if (legacyScript && legacyScript.trim().length > 0) {
|
||||
script = {
|
||||
res: legacyScript
|
||||
};
|
||||
@@ -70,7 +70,7 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: _.get(json, 'seq'),
|
||||
seq: _.get(json, 'seq')
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
@@ -81,7 +81,7 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: script,
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
tests: _.get(json, 'request.tests', '')
|
||||
};
|
||||
|
||||
const newBruContent = jsonToBruV2(bruJson);
|
||||
@@ -89,11 +89,11 @@ const migrateLegacyBruFile = async (bruContent, pathname) => {
|
||||
await writeFile(pathname, newBruContent);
|
||||
|
||||
return newBruContent;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isLegacyEnvFile,
|
||||
migrateLegacyEnvFile,
|
||||
isLegacyBruFile,
|
||||
migrateLegacyBruFile
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* we maintain a cache of request uids to ensure that we
|
||||
* preserve the same uid for a request even when the request
|
||||
* moves to a different location
|
||||
*
|
||||
*
|
||||
* In the past, we used to generate unique ids based on the
|
||||
* pathname of the request, but we faced problems when implementing
|
||||
* functionality where the user can move the request to a different
|
||||
|
||||
@@ -5,7 +5,7 @@ const { BrowserWindow, app, Menu } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
const LastOpenedCollections = require('./app/last-opened-collections');
|
||||
const LastOpenedCollections = require('./store/last-opened-collections');
|
||||
const registerNetworkIpc = require('./ipc/network');
|
||||
const registerCollectionsIpc = require('./ipc/collection');
|
||||
const Watcher = require('./app/watcher');
|
||||
@@ -18,7 +18,7 @@ setContentSecurityPolicy(`
|
||||
connect-src * 'unsafe-inline';
|
||||
base-uri 'none';
|
||||
form-action 'none';
|
||||
img-src 'self' data:image/svg+xml
|
||||
img-src 'self' data:image/svg+xml;
|
||||
`);
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
@@ -35,8 +35,8 @@ app.on('ready', async () => {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js")
|
||||
},
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
const url = isDev
|
||||
@@ -50,7 +50,7 @@ app.on('ready', async () => {
|
||||
mainWindow.loadURL(url);
|
||||
watcher = new Watcher();
|
||||
|
||||
mainWindow.webContents.on('new-window', function(e, url) {
|
||||
mainWindow.webContents.on('new-window', function (e, url) {
|
||||
e.preventDefault();
|
||||
require('electron').shell.openExternal(url);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { ipcMain } = require('electron');
|
||||
const {
|
||||
envJsonToBru,
|
||||
bruToJson,
|
||||
jsonToBru
|
||||
} = require('../bru');
|
||||
const { ipcMain, shell } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
|
||||
|
||||
const {
|
||||
isValidPathname,
|
||||
@@ -15,13 +11,23 @@ const {
|
||||
isDirectory,
|
||||
browseDirectory,
|
||||
createDirectory,
|
||||
searchForBruFiles
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName
|
||||
} = require('../utils/filesystem');
|
||||
const { stringifyJson } = require('../utils/common');
|
||||
const { openCollectionDialog, openCollection } = require('../app/collections');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
||||
const { setPreferences } = require("../app/preferences");
|
||||
const { setPreferences } = require('../store/preferences');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
const envHasSecrets = (environment = {}) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||
|
||||
return secrets && secrets.length > 0;
|
||||
};
|
||||
|
||||
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
// browse directory
|
||||
@@ -36,35 +42,39 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
// create collection
|
||||
ipcMain.handle('renderer:create-collection', async (event, collectionName, collectionFolderName, collectionLocation) => {
|
||||
try {
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)){
|
||||
throw new Error(`collection: ${dirPath} already exists`);
|
||||
ipcMain.handle(
|
||||
'renderer:create-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation) => {
|
||||
try {
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
throw new Error(`collection: ${dirPath} already exists`);
|
||||
}
|
||||
|
||||
if (!isValidPathname(dirPath)) {
|
||||
throw new Error(`collection: invalid pathname - ${dir}`);
|
||||
}
|
||||
|
||||
await createDirectory(dirPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(dirPath);
|
||||
const brunoConfig = {
|
||||
version: '1',
|
||||
name: collectionName,
|
||||
type: 'collection'
|
||||
};
|
||||
const content = await stringifyJson(brunoConfig);
|
||||
await writeFile(path.join(dirPath, 'bruno.json'), content);
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if(!isValidPathname(dirPath)) {
|
||||
throw new Error(`collection: invalid pathname - ${dir}`);
|
||||
}
|
||||
|
||||
await createDirectory(dirPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(dirPath);
|
||||
const content = await stringifyJson({
|
||||
version: '1',
|
||||
name: collectionName,
|
||||
type: 'collection'
|
||||
});
|
||||
await writeFile(path.join(dirPath, 'bruno.json'), content);
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, collectionName);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// rename collection
|
||||
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
|
||||
@@ -94,7 +104,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// new request
|
||||
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
|
||||
try {
|
||||
if (fs.existsSync(pathname)){
|
||||
if (fs.existsSync(pathname)) {
|
||||
throw new Error(`path: ${pathname} already exists`);
|
||||
}
|
||||
|
||||
@@ -108,7 +118,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// save request
|
||||
ipcMain.handle('renderer:save-request', async (event, pathname, request) => {
|
||||
try {
|
||||
if (!fs.existsSync(pathname)){
|
||||
if (!fs.existsSync(pathname)) {
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
@@ -123,12 +133,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
if (!fs.existsSync(envDirPath)){
|
||||
if (!fs.existsSync(envDirPath)) {
|
||||
await createDirectory(envDirPath);
|
||||
}
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${name}.bru`);
|
||||
if (fs.existsSync(envFilePath)){
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} already exists`);
|
||||
}
|
||||
|
||||
@@ -145,15 +155,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
if (!fs.existsSync(envDirPath)){
|
||||
if (!fs.existsSync(envDirPath)) {
|
||||
await createDirectory(envDirPath);
|
||||
}
|
||||
|
||||
const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
|
||||
if (!fs.existsSync(envFilePath)){
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} does not exist`);
|
||||
}
|
||||
|
||||
if (envHasSecrets(environment)) {
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const content = envJsonToBru(environment);
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
@@ -166,16 +180,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
|
||||
if (!fs.existsSync(envFilePath)){
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} does not exist`);
|
||||
}
|
||||
|
||||
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
|
||||
if (fs.existsSync(newEnvFilePath)){
|
||||
if (fs.existsSync(newEnvFilePath)) {
|
||||
throw new Error(`environment: ${newEnvFilePath} already exists`);
|
||||
}
|
||||
|
||||
fs.renameSync(envFilePath, newEnvFilePath);
|
||||
|
||||
environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -186,11 +202,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
try {
|
||||
const envDirPath = path.join(collectionPathname, 'environments');
|
||||
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
|
||||
if (!fs.existsSync(envFilePath)){
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
throw new Error(`environment: ${envFilePath} does not exist`);
|
||||
}
|
||||
|
||||
fs.unlinkSync(envFilePath);
|
||||
|
||||
environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -199,18 +217,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||
try {
|
||||
if (!fs.existsSync(oldPath)){
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
throw new Error(`path: ${oldPath} does not exist`);
|
||||
}
|
||||
if (fs.existsSync(newPath)){
|
||||
if (fs.existsSync(newPath)) {
|
||||
throw new Error(`path: ${oldPath} already exists`);
|
||||
}
|
||||
|
||||
// if its directory, rename and return
|
||||
if(isDirectory(oldPath)) {
|
||||
if (isDirectory(oldPath)) {
|
||||
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||
|
||||
for(let bruFile of bruFilesAtSource) {
|
||||
for (let bruFile of bruFilesAtSource) {
|
||||
const newBruFilePath = bruFile.replace(oldPath, newPath);
|
||||
moveRequestUid(bruFile, newBruFilePath);
|
||||
}
|
||||
@@ -218,7 +236,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
const isBru = hasBruExtension(oldPath);
|
||||
if(!isBru) {
|
||||
if (!isBru) {
|
||||
throw new Error(`path: ${oldPath} is not a bru file`);
|
||||
}
|
||||
|
||||
@@ -241,8 +259,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// new folder
|
||||
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
|
||||
try {
|
||||
if (!fs.existsSync(pathname)){
|
||||
fs.mkdirSync(pathname);
|
||||
if (!fs.existsSync(pathname)) {
|
||||
fs.mkdirSync(pathname);
|
||||
} else {
|
||||
return Promise.reject(new Error('The directory already exists'));
|
||||
}
|
||||
@@ -254,20 +272,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// delete file/folder
|
||||
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
|
||||
try {
|
||||
if(type === 'folder') {
|
||||
if(!fs.existsSync(pathname)) {
|
||||
if (type === 'folder') {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
return Promise.reject(new Error('The directory does not exist'));
|
||||
}
|
||||
|
||||
// delete the request uid mappings
|
||||
const bruFilesAtSource = await searchForBruFiles(pathname);
|
||||
for(let bruFile of bruFilesAtSource) {
|
||||
for (let bruFile of bruFilesAtSource) {
|
||||
deleteRequestUid(bruFile);
|
||||
}
|
||||
|
||||
fs.rmSync(pathname, { recursive: true, force: true});
|
||||
fs.rmSync(pathname, { recursive: true, force: true });
|
||||
} else if (['http-request', 'graphql-request'].includes(type)) {
|
||||
if(!fs.existsSync(pathname)) {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
return Promise.reject(new Error('The file does not exist'));
|
||||
}
|
||||
|
||||
@@ -283,13 +301,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:open-collection', () => {
|
||||
if(watcher && mainWindow) {
|
||||
if (watcher && mainWindow) {
|
||||
openCollectionDialog(mainWindow, watcher);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
|
||||
if(watcher && mainWindow) {
|
||||
if (watcher && mainWindow) {
|
||||
console.log(`watcher stopWatching: ${collectionPath}`);
|
||||
watcher.removeWatcher(collectionPath, mainWindow);
|
||||
lastOpenedCollections.remove(collectionPath);
|
||||
@@ -298,16 +316,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
|
||||
try {
|
||||
let collectionName = collection.name;
|
||||
let collectionName = sanitizeDirectoryName(collection.name);
|
||||
let collectionPath = path.join(collectionLocation, collectionName);
|
||||
|
||||
if (fs.existsSync(collectionPath)){
|
||||
if (fs.existsSync(collectionPath)) {
|
||||
throw new Error(`collection: ${collectionPath} already exists`);
|
||||
}
|
||||
|
||||
// Recursive function to parse the collection items and create files/folders
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach(item => {
|
||||
items.forEach((item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = jsonToBru(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
@@ -317,7 +335,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const folderPath = path.join(currentPath, item.name);
|
||||
fs.mkdirSync(folderPath);
|
||||
|
||||
if(item.items && item.items.length) {
|
||||
if (item.items && item.items.length) {
|
||||
parseCollectionItems(item.items, folderPath);
|
||||
}
|
||||
}
|
||||
@@ -326,11 +344,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
const parseEnvironments = (environments = [], collectionPath) => {
|
||||
const envDirPath = path.join(collectionPath, 'environments');
|
||||
if(!fs.existsSync(envDirPath)){
|
||||
if (!fs.existsSync(envDirPath)) {
|
||||
fs.mkdirSync(envDirPath);
|
||||
}
|
||||
|
||||
environments.forEach(env => {
|
||||
environments.forEach((env) => {
|
||||
const content = envJsonToBru(env);
|
||||
const filePath = path.join(envDirPath, `${env.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
@@ -340,14 +358,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
await createDirectory(collectionPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
const content = await stringifyJson({
|
||||
const brunoConfig = {
|
||||
version: '1',
|
||||
name: collection.name,
|
||||
name: collectionName,
|
||||
type: 'collection'
|
||||
});
|
||||
};
|
||||
const content = await stringifyJson(brunoConfig);
|
||||
await writeFile(path.join(collectionPath, 'bruno.json'), content);
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, collectionName);
|
||||
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid);
|
||||
|
||||
lastOpenedCollections.add(collectionPath);
|
||||
@@ -355,7 +374,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// create folder and files based on collection
|
||||
await parseCollectionItems(collection.items, collectionPath);
|
||||
await parseEnvironments(collection.environments, collectionPath);
|
||||
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -363,11 +381,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
|
||||
try {
|
||||
for(let item of itemsToResequence) {
|
||||
for (let item of itemsToResequence) {
|
||||
const bru = fs.readFileSync(item.pathname, 'utf8');
|
||||
const jsonData = bruToJson(bru);
|
||||
|
||||
if(jsonData.seq !== item.seq) {
|
||||
if (jsonData.seq !== item.seq) {
|
||||
jsonData.seq = item.seq;
|
||||
const content = jsonToBru(jsonData);
|
||||
await writeFile(item.pathname, content);
|
||||
@@ -397,17 +415,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const folderName = path.basename(folderPath);
|
||||
const newFolderPath = path.join(destinationPath, folderName);
|
||||
|
||||
if(!fs.existsSync(folderPath)) {
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
throw new Error(`folder: ${folderPath} does not exist`);
|
||||
}
|
||||
|
||||
if(fs.existsSync(newFolderPath)) {
|
||||
if (fs.existsSync(newFolderPath)) {
|
||||
throw new Error(`folder: ${newFolderPath} already exists`);
|
||||
}
|
||||
|
||||
const bruFilesAtSource = await searchForBruFiles(folderPath);
|
||||
|
||||
for(let bruFile of bruFilesAtSource) {
|
||||
for (let bruFile of bruFilesAtSource) {
|
||||
const newBruFilePath = bruFile.replace(folderPath, newFolderPath);
|
||||
moveRequestUid(bruFile, newBruFilePath);
|
||||
}
|
||||
@@ -422,9 +440,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// reload last opened collections
|
||||
const lastOpened = lastOpenedCollections.getAll();
|
||||
|
||||
if(lastOpened && lastOpened.length) {
|
||||
for(let collectionPath of lastOpened) {
|
||||
if(isDirectory(collectionPath)) {
|
||||
if (lastOpened && lastOpened.length) {
|
||||
for (let collectionPath of lastOpened) {
|
||||
if (isDirectory(collectionPath)) {
|
||||
openCollection(mainWindow, watcher, collectionPath, {
|
||||
dontSendDisplayErrors: true
|
||||
});
|
||||
@@ -436,25 +454,39 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:set-preferences', async (event, preferences) => {
|
||||
setPreferences(preferences);
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionUid) => {
|
||||
try {
|
||||
const brunoConfigPath = path.join(collectionPath, 'bruno.json');
|
||||
const content = await stringifyJson(brunoConfig);
|
||||
await writeFile(brunoConfigPath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
ipcMain.on('main:open-collection', () => {
|
||||
if(watcher && mainWindow) {
|
||||
if (watcher && mainWindow) {
|
||||
openCollectionDialog(mainWindow, watcher);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('main:open-docs', () => {
|
||||
const docsURL = 'https://docs.usebruno.com';
|
||||
shell.openExternal(docsURL);
|
||||
});
|
||||
|
||||
ipcMain.on('main:collection-opened', (win, pathname, uid) => {
|
||||
watcher.addWatcher(win, pathname, uid);
|
||||
lastOpenedCollections.add(pathname);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
registerRendererEventHandlers(mainWindow, watcher, lastOpenedCollections);
|
||||
registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = registerCollectionsIpc;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user