mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
64 Commits
feature/en
...
feature/au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1e330dc5 | ||
|
|
51ee37cf96 | ||
|
|
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 | ||
|
|
aeb29393c5 | ||
|
|
0866d33858 | ||
|
|
ad905d1a0a |
@@ -1,6 +1,6 @@
|
||||
## 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
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"about-window": "^1.15.2",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
@@ -39,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",
|
||||
@@ -53,6 +53,7 @@
|
||||
"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 (
|
||||
|
||||
@@ -12,12 +12,14 @@ const Wrapper = styled.div`
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
padding: 4px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
&:nth-child(1),
|
||||
&:nth-child(4),
|
||||
&:nth-child(5) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,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 = () => {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.auth-mode-selector {
|
||||
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default AuthMode;
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
export default Wrapper;
|
||||
32
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
32
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const onEdit = (value) => {
|
||||
// dispatch(
|
||||
// updateRequestBody({
|
||||
// content: value,
|
||||
// itemUid: item.uid,
|
||||
// collectionUid: collection.uid
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
if (authMode === 'basic') {
|
||||
return <div>Basic Auth</div>;
|
||||
}
|
||||
|
||||
if (authMode === 'bearer') {
|
||||
return <div>Bearer Token</div>;
|
||||
}
|
||||
|
||||
return <StyledWrapper className="w-full">No Auth</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Auth from 'components/RequestPane/Auth';
|
||||
import AuthMode from 'components/RequestPane/Auth/AuthMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
@@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@@ -62,7 +67,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 +78,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>
|
||||
@@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
@@ -95,13 +103,16 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
{focusedTab.requestPaneTab === 'auth' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -72,19 +72,27 @@ 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'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -111,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);
|
||||
@@ -119,8 +120,7 @@ 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} />;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,10 @@ const RequestTabPanel = () => {
|
||||
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,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconFiles, IconRun, IconEye } from '@tabler/icons';
|
||||
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
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 }) => {
|
||||
@@ -12,8 +11,10 @@ const CollectionToolBar = ({ collection }) => {
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(
|
||||
toggleRunnerView({
|
||||
collectionUid: collection.uid
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -28,6 +29,16 @@ const CollectionToolBar = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
@@ -42,6 +53,9 @@ const CollectionToolBar = ({ 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>
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
import React from 'react';
|
||||
import { IconVariable } from '@tabler/icons';
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, text }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">{text}</span>
|
||||
</div>
|
||||
<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
|
||||
|
||||
@@ -57,10 +57,10 @@ const RequestTab = ({ tab, collection }) => {
|
||||
return color;
|
||||
};
|
||||
|
||||
if (tab.type === 'variables') {
|
||||
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" />
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
</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,13 @@ 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">{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 = () => (
|
||||
|
||||
@@ -36,7 +36,7 @@ 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'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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.1</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,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;
|
||||
|
||||
@@ -34,7 +34,7 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
|
||||
<div className="muted">No environment selected</div>
|
||||
<div className="muted text-xs">No environment selected</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
{enabledEnvVars.length > 0 ? (
|
||||
<KeyValueExplorer data={envVarsObj} theme={theme} />
|
||||
) : (
|
||||
<div className="muted">No environment variables found</div>
|
||||
<div className="muted text-xs">No environment variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -70,7 +70,7 @@ const CollectionVariables = ({ collection, theme }) => {
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
|
||||
) : (
|
||||
<div className="muted">No collection variables found</div>
|
||||
<div className="muted text-xs">No collection variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -85,6 +85,12 @@ const VariablesEditor = ({ collection }) => {
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
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';
|
||||
@@ -27,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) => {
|
||||
@@ -128,6 +129,7 @@ const useCollectionTreeSync = () => {
|
||||
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();
|
||||
@@ -140,6 +142,7 @@ const useCollectionTreeSync = () => {
|
||||
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);
|
||||
|
||||
@@ -42,8 +42,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;
|
||||
|
||||
@@ -723,11 +723,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 +746,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);
|
||||
|
||||
@@ -221,7 +229,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) {
|
||||
@@ -555,6 +563,20 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection && collection.items && collection.items.length) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.auth.mode = action.payload.mode;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestBodyMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1008,30 +1030,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);
|
||||
@@ -1141,13 +1139,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1155,6 +1152,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
export const {
|
||||
createCollection,
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
updateLastAction,
|
||||
@@ -1170,7 +1168,7 @@ export const {
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
saveRequest,
|
||||
newEphermalHttpRequest,
|
||||
newEphemeralHttpRequest,
|
||||
collectionClicked,
|
||||
collectionFolderClicked,
|
||||
requestUrlChanged,
|
||||
@@ -1186,6 +1184,7 @@ export const {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam,
|
||||
updateRequestAuthMode,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
@@ -1207,13 +1206,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,
|
||||
@@ -20,8 +24,8 @@ export const tabsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.payload.type === 'variables') {
|
||||
const tab = find(state.tabs, (t) => t.collectionUid === action.payload.collectionUid && t.type === 'variables');
|
||||
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;
|
||||
@@ -92,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;
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -445,6 +445,22 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
return label;
|
||||
};
|
||||
|
||||
export const humanizeRequestAuthMode = (mode) => {
|
||||
let label = 'No Auth';
|
||||
switch (mode) {
|
||||
case 'basic': {
|
||||
label = 'Basic Auth';
|
||||
break;
|
||||
}
|
||||
case 'bearer': {
|
||||
label = 'Bearer Token';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
export const refreshUidsInItem = (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
|
||||
@@ -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,32 @@ 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 '';
|
||||
};
|
||||
|
||||
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,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 { 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';
|
||||
@@ -125,6 +127,11 @@ const builder = async (yargs) => {
|
||||
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',
|
||||
description: 'Allow insecure server connections'
|
||||
@@ -136,12 +143,16 @@ const builder = async (yargs) => {
|
||||
.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, envVar, insecure, r: recursive } = argv;
|
||||
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
// todo
|
||||
@@ -154,6 +165,9 @@ const handler = async function (argv) {
|
||||
return;
|
||||
}
|
||||
|
||||
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
|
||||
if (filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if (!pathExists) {
|
||||
@@ -225,30 +239,40 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
const files = fs.readdirSync(filename);
|
||||
@@ -263,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;
|
||||
@@ -275,28 +297,51 @@ const handler = async function (argv) {
|
||||
|
||||
bruJsons = getBruFilesRecursively(filename);
|
||||
}
|
||||
}
|
||||
|
||||
let assertionResults = [];
|
||||
let testResults = [];
|
||||
for (const iter of bruJsons) {
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
const result = await runSingleRequest(
|
||||
bruFilepath,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig
|
||||
);
|
||||
|
||||
for (const iter of bruJsons) {
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars);
|
||||
if (result) {
|
||||
testrunResults.push(result);
|
||||
const { assertionResults: _assertionResults, testResults: _testResults } = result;
|
||||
|
||||
if (result) {
|
||||
const { assertionResults: _assertionResults, testResults: _testResults } = result;
|
||||
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
assertionResults = assertionResults.concat(_assertionResults);
|
||||
testResults = testResults.concat(_testResults);
|
||||
}
|
||||
}
|
||||
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
const summary = printRunSummary(assertionResults, testResults);
|
||||
console.log(chalk.dim(chalk.grey('Ran all requests.')));
|
||||
|
||||
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
|
||||
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');
|
||||
|
||||
@@ -1,33 +1,74 @@
|
||||
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') {
|
||||
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') {
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
@@ -41,7 +82,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}) => {
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
@@ -57,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 {
|
||||
@@ -32,7 +40,14 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
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
|
||||
@@ -44,12 +59,39 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
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);
|
||||
@@ -95,7 +137,8 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,7 +152,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +193,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
@@ -164,6 +211,18 @@ 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
|
||||
};
|
||||
@@ -183,7 +242,8 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +257,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,7 +298,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
|
||||
err.response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v0.14.1",
|
||||
"version": "v0.16.2",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -14,10 +14,11 @@
|
||||
"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",
|
||||
|
||||
@@ -5,20 +5,12 @@ 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 {
|
||||
@@ -67,15 +59,15 @@ const openCollectionDialog = async (win, watcher) => {
|
||||
const openCollection = async (win, watcher, collectionPath, options = {}) => {
|
||||
if (!watcher.hasWatcher(collectionPath)) {
|
||||
try {
|
||||
const { name } = await getCollectionConfigFile(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) {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ const template = [
|
||||
click: () =>
|
||||
openAboutWindow({
|
||||
product_name: 'Bruno',
|
||||
icon_path: join(__dirname, '../../resources/icons/png/256x256.png'),
|
||||
icon_path: join(process.cwd(), '/resources/icons/png/256x256.png'),
|
||||
homepage: 'https://www.usebruno.com/',
|
||||
package_json_dir: join(__dirname, '../..')
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ 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();
|
||||
@@ -30,6 +31,13 @@ const isDotEnvFile = (pathname, collectionPath) => {
|
||||
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');
|
||||
@@ -167,6 +175,17 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
console.log(`watcher add: ${pathname}`);
|
||||
|
||||
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');
|
||||
@@ -281,6 +300,23 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
};
|
||||
|
||||
const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
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 (isDotEnvFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
@@ -378,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: {
|
||||
|
||||
@@ -7,7 +7,7 @@ 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'));
|
||||
@@ -61,6 +61,7 @@ const bruToJson = (bru) => {
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
@@ -69,6 +70,7 @@ const bruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
@@ -104,10 +106,12 @@ const jsonToBru = (json) => {
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
query: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
|
||||
@@ -57,14 +57,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
await createDirectory(dirPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(dirPath);
|
||||
const content = await stringifyJson({
|
||||
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, collectionName);
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
|
||||
return;
|
||||
@@ -356,14 +357,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
await createDirectory(collectionPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
const content = await stringifyJson({
|
||||
const brunoConfig = {
|
||||
version: '1',
|
||||
name: collection.name,
|
||||
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);
|
||||
@@ -451,6 +453,16 @@ 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) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ const interpolateVars = require('./interpolate-vars');
|
||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||
const { getPreferences } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@@ -101,6 +102,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
|
||||
try {
|
||||
// make axios work in node using form data
|
||||
@@ -127,7 +129,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
request,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
if (result) {
|
||||
@@ -150,7 +153,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -161,7 +165,31 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
// proxy configuration
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
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;
|
||||
}
|
||||
|
||||
interpolateVars(request, envVars, collectionVariables, processEnvVars);
|
||||
|
||||
@@ -224,7 +252,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
if (result) {
|
||||
@@ -248,7 +277,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -292,7 +322,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
@@ -365,7 +396,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
@@ -510,6 +542,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
|
||||
try {
|
||||
// make axios work in node using form data
|
||||
@@ -554,7 +587,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -564,7 +598,31 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
// proxy configuration
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
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, envVars, collectionVariables, processEnvVars);
|
||||
@@ -607,7 +665,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
if (result) {
|
||||
@@ -630,7 +689,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -672,7 +732,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -750,7 +811,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
const Handlebars = require('handlebars');
|
||||
const { each, forOwn, cloneDeep } = require('lodash');
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
forOwn(headers, (value, key) => {
|
||||
if (key && key.toLowerCase() === 'content-type') {
|
||||
contentType = value;
|
||||
}
|
||||
});
|
||||
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateEnvVars = (str, processEnvVars) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
@@ -51,10 +62,13 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
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') {
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
@@ -68,7 +82,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
request.data = interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
@@ -84,6 +98,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
19
packages/bruno-electron/src/store/bruno-config.js
Normal file
19
packages/bruno-electron/src/store/bruno-config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* This modules stores the configs loaded from bruno.json
|
||||
*/
|
||||
|
||||
const config = {};
|
||||
|
||||
// collectionUid is a hash based on the collection path)
|
||||
const getBrunoConfig = (collectionUid) => {
|
||||
return config[collectionUid] || {};
|
||||
};
|
||||
|
||||
const setBrunoConfig = (collectionUid, brunoConfig) => {
|
||||
config[collectionUid] = brunoConfig;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getBrunoConfig,
|
||||
setBrunoConfig
|
||||
};
|
||||
22
packages/bruno-graphql-docs/license.md
Normal file
22
packages/bruno-graphql-docs/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/graphql-docs",
|
||||
"version": "0.1.0",
|
||||
"license" : "MIT",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"files": [
|
||||
|
||||
22
packages/bruno-js/license.md
Normal file
22
packages/bruno-js/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -18,7 +19,9 @@
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.26.0",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"crypto-js": "^4.1.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
const Handlebars = require('handlebars');
|
||||
const { cloneDeep } = require('lodash');
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, collectionVariables) {
|
||||
constructor(envVariables, collectionVariables, processEnvVars) {
|
||||
this.envVariables = envVariables;
|
||||
this.collectionVariables = collectionVariables;
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
}
|
||||
|
||||
_interpolateEnvVar = (str) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
return template({
|
||||
process: {
|
||||
env: {
|
||||
...this.processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getEnvName() {
|
||||
return this.envVariables.__name__;
|
||||
}
|
||||
|
||||
getProcessEnv(key) {
|
||||
return process.env[key];
|
||||
return this.processEnvVars[key];
|
||||
}
|
||||
|
||||
getEnvVar(key) {
|
||||
return this.envVariables[key];
|
||||
return this._interpolateEnvVar(this.envVariables[key]);
|
||||
}
|
||||
|
||||
setEnvVar(key, value) {
|
||||
|
||||
@@ -46,6 +46,10 @@ class BrunoRequest {
|
||||
setBody(data) {
|
||||
this.req.data = data;
|
||||
}
|
||||
|
||||
setMaxRedirects(maxRedirects) {
|
||||
this.req.maxRedirects = maxRedirects;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrunoRequest;
|
||||
|
||||
@@ -21,13 +21,22 @@ const uuid = require('uuid');
|
||||
const nanoid = require('nanoid');
|
||||
const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const chai = require('chai');
|
||||
const CryptoJS = require('crypto-js');
|
||||
|
||||
class ScriptRuntime {
|
||||
constructor() {}
|
||||
|
||||
async runRequestScript(script, request, envVariables, collectionVariables, collectionPath, onConsoleLog) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
async runRequestScript(
|
||||
script,
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
) {
|
||||
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const context = {
|
||||
@@ -73,6 +82,7 @@ class ScriptRuntime {
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS
|
||||
}
|
||||
@@ -87,8 +97,17 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
async runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
async runResponseScript(
|
||||
script,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
) {
|
||||
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
|
||||
@@ -20,8 +20,17 @@ const CryptoJS = require('crypto-js');
|
||||
class TestRuntime {
|
||||
constructor() {}
|
||||
|
||||
runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog) {
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
runTests(
|
||||
testsFile,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars
|
||||
) {
|
||||
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
@@ -74,6 +83,7 @@ class TestRuntime {
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
chai,
|
||||
'crypto-js': CryptoJS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
|
||||
class VarsRuntime {
|
||||
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) {
|
||||
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath, processEnvVars) {
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if (!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const bruContext = {
|
||||
@@ -34,13 +34,13 @@ class VarsRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath) {
|
||||
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath, processEnvVars) {
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if (!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, collectionVariables);
|
||||
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const JS_KEYWORDS = `
|
||||
.filter((word) => word.length > 0);
|
||||
|
||||
/**
|
||||
* Creates a function from a Javascript expression
|
||||
* Creates a function from a JavaScript expression
|
||||
*
|
||||
* When the function is called, the variables used in this expression are picked up from the context
|
||||
*
|
||||
@@ -119,7 +119,7 @@ const createResponseParser = (response = {}) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Objects that are created inside vm2 execution context result in an serilaization error when sent to the renderer process
|
||||
* Objects that are created inside vm2 execution context result in an serialization error when sent to the renderer process
|
||||
* Error sending from webFrameMain: Error: Failed to serialize arguments
|
||||
* at s.send (node:electron/js2c/browser_init:169:631)
|
||||
* at g.send (node:electron/js2c/browser_init:165:2156)
|
||||
|
||||
22
packages/bruno-lang/license.md
Normal file
22
packages/bruno-lang/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/lang",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"license" : "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -66,7 +66,7 @@ const bodyXmlTag = between(bodyXmlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((
|
||||
* We have deprecated form-url-encoded type in body tag, it was a misspelling on my part
|
||||
* The new type is form-urlencoded
|
||||
*
|
||||
* Very few peope would have used this. I launched this to the public on 22 Jan 2023
|
||||
* Very few people would have used this. I launched this to the public on 22 Jan 2023
|
||||
* And I am making the change on 23 Jan 2023
|
||||
*
|
||||
* This deprecated tag can be removed on 1 April 2023
|
||||
|
||||
@@ -22,7 +22,8 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
*
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | query | headers | bodies | varsandassert | script | tests | docs)*
|
||||
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
auths = authbasic | authbearer
|
||||
bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
|
||||
@@ -75,6 +76,9 @@ const grammar = ohm.grammar(`Bru {
|
||||
varsres = "vars:post-response" dictionary
|
||||
assert = "assert" assertdictionary
|
||||
|
||||
authbasic = "auth:basic" dictionary
|
||||
authbearer = "auth:bearer" dictionary
|
||||
|
||||
body = "body" st* "{" nl* textblock tagend
|
||||
bodyjson = "body:json" st* "{" nl* textblock tagend
|
||||
bodytext = "body:text" st* "{" nl* textblock tagend
|
||||
@@ -92,13 +96,21 @@ const grammar = ohm.grammar(`Bru {
|
||||
docs = "docs" st* "{" nl* textblock tagend
|
||||
}`);
|
||||
|
||||
const mapPairListToKeyValPairs = (pairList = []) => {
|
||||
const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
if (!pairList.length) {
|
||||
return [];
|
||||
}
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
|
||||
if (!parseEnabled) {
|
||||
return {
|
||||
name,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
let enabled = true;
|
||||
if (name && name.length && name.charAt(0) === '~') {
|
||||
name = name.slice(1);
|
||||
@@ -282,6 +294,33 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
headers: mapPairListToKeyValPairs(dictionary.ast)
|
||||
};
|
||||
},
|
||||
authbasic(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const usernameKey = _.find(auth, { name: 'username' });
|
||||
const passwordKey = _.find(auth, { name: 'password' });
|
||||
const username = usernameKey ? usernameKey.value : '';
|
||||
const password = passwordKey ? passwordKey.value : '';
|
||||
return {
|
||||
auth: {
|
||||
basic: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
authbearer(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const tokenKey = _.find(auth, { name: 'token' });
|
||||
const token = tokenKey ? tokenKey.value : '';
|
||||
return {
|
||||
auth: {
|
||||
bearer: {
|
||||
token
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
bodyformurlencoded(_1, dictionary) {
|
||||
return {
|
||||
body: {
|
||||
|
||||
@@ -13,7 +13,7 @@ const stripLastLine = (text) => {
|
||||
};
|
||||
|
||||
const jsonToBru = (json) => {
|
||||
const { meta, http, query, headers, body, script, tests, vars, assertions, docs } = json;
|
||||
const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json;
|
||||
|
||||
let bru = '';
|
||||
|
||||
@@ -82,6 +82,23 @@ const jsonToBru = (json) => {
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (auth && auth.basic) {
|
||||
bru += `auth:basic {
|
||||
${indentString(`username: ${auth.basic.username}`)}
|
||||
${indentString(`password: ${auth.basic.password}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (auth && auth.bearer) {
|
||||
bru += `auth:bearer {
|
||||
${indentString(`token: ${auth.bearer.token}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (body && body.json && body.json.length) {
|
||||
bru += `body:json {
|
||||
${indentString(body.json)}
|
||||
|
||||
@@ -21,6 +21,15 @@ headers {
|
||||
~transaction-id: {{transactionId}}
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: john
|
||||
password: secret
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: 123
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "world"
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"basic": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
},
|
||||
"bearer": {
|
||||
"token": "123"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"json": "{\n \"hello\": \"world\"\n}",
|
||||
"text": "This is a text body",
|
||||
|
||||
22
packages/bruno-query/license.md
Normal file
22
packages/bruno-query/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/query",
|
||||
"version": "0.1.0",
|
||||
"license" : "MIT",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
22
packages/bruno-schema/license.md
Normal file
22
packages/bruno-schema/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -69,6 +69,27 @@ const requestBodySchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBasicSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBearerSchema = Yup.object({
|
||||
token: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authSchema = Yup.object({
|
||||
mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'),
|
||||
basic: authBasicSchema.nullable(),
|
||||
bearer: authBearerSchema.nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
// Right now, the request schema is very tightly coupled with http request
|
||||
// As we introduce more request types in the future, we will improve the definition to support
|
||||
// schema structure based on other request type
|
||||
@@ -77,6 +98,7 @@ const requestSchema = Yup.object({
|
||||
method: requestMethodSchema,
|
||||
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
||||
params: Yup.array().of(keyValueSchema).required('params are required'),
|
||||
auth: authSchema,
|
||||
body: requestBodySchema,
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
@@ -124,11 +146,11 @@ const collectionSchema = Yup.object({
|
||||
.nullable(),
|
||||
environments: environmentsSchema,
|
||||
pathname: Yup.string().nullable(),
|
||||
showRunner: Yup.boolean(),
|
||||
runnerResult: Yup.object({
|
||||
items: Yup.array()
|
||||
}),
|
||||
collectionVariables: Yup.object()
|
||||
collectionVariables: Yup.object(),
|
||||
brunoConfig: Yup.object()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"displayName": "Bruno",
|
||||
"description": "Bruno support for Visual Studio Code.",
|
||||
"version": "0.0.1",
|
||||
"license" : "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.74.0"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ exports.HomePage = class HomePage {
|
||||
|
||||
// sample collection
|
||||
this.loadSampleCollectionSuccessToast = page.getByText('Sample Collection loaded successfully');
|
||||
this.sampeCollectionSelector = page.locator('#sidebar-collection-name');
|
||||
this.sampleCollectionSelector = page.locator('#sidebar-collection-name');
|
||||
this.getUsersSelector = page.getByText('Users');
|
||||
this.getSingleUserSelector = page.getByText('Single User');
|
||||
this.getUserNotFoundSelector = page.getByText('User Not Found');
|
||||
@@ -43,7 +43,7 @@ exports.HomePage = class HomePage {
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
await this.sampeCollectionSelector.click();
|
||||
await this.sampleCollectionSelector.click();
|
||||
await this.getUsersSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user