mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-22 23:02:40 +01:00
Merge remote-tracking branch 'upstream/main' into bug/correct-result-reporting
This commit is contained in:
commit
da4e96d1ef
@ -19,7 +19,7 @@ Libraries we use
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||||
|
|
||||||
### Lets start coding
|
### Lets start coding
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
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.
|
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
|
### Dependencies
|
||||||
* NodeJS v18
|
|
||||||
|
- NodeJS v18
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|
||||||
@ -15,7 +16,6 @@ nvm use
|
|||||||
npm i --legacy-peer-deps
|
npm i --legacy-peer-deps
|
||||||
|
|
||||||
# build graphql docs
|
# build graphql docs
|
||||||
# note: you can for now ignore the error thrown while building the graphql docs
|
|
||||||
npm run build:graphql-docs
|
npm run build:graphql-docs
|
||||||
|
|
||||||
# build bruno query
|
# build bruno query
|
||||||
|
@ -38,6 +38,5 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"rollup": "3.2.5"
|
"rollup": "3.2.5"
|
||||||
},
|
}
|
||||||
"dependencies": {}
|
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,11 @@
|
|||||||
"graphiql": "^1.5.9",
|
"graphiql": "^1.5.9",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-request": "^3.7.0",
|
"graphql-request": "^3.7.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"httpsnippet": "^3.0.1",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
|
"know-your-http-well": "^0.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
|
@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
className="h-full"
|
className="h-full w-full"
|
||||||
aria-label="Code Editor"
|
aria-label="Code Editor"
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
this._node = node;
|
this._node = node;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
|
||||||
&.modal--animate-out {
|
&.modal--animate-out {
|
||||||
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
|
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
|
||||||
|
@ -197,10 +197,11 @@ const AssertionRow = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={assertion.enabled}
|
checked={assertion.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
<button tabIndex="-1" onClick={() => handleRemoveAssertion(assertion)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
|
||||||
|
.auth-mode-selector {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.auth-mode-label {
|
||||||
|
color: ${(props) => props.theme.colors.text.yellow};
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 auth-mode-label select-none">
|
||||||
|
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" 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,16 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line-editor-wrapper {
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: solid 1px ${(props) => props.theme.input.border};
|
||||||
|
background-color: ${(props) => props.theme.input.bg};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { useTheme } from 'providers/Theme';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
|
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const BasicAuth = ({ item, collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||||
|
|
||||||
|
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
|
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
|
||||||
|
const handleUsernameChange = (username) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'basic',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username: username,
|
||||||
|
password: basicAuth.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (password) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'basic',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username: basicAuth.username,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Username</label>
|
||||||
|
<div className="single-line-editor-wrapper mb-2">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={basicAuth.username || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleUsernameChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="block font-medium mb-2">Password</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={basicAuth.password || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handlePasswordChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicAuth;
|
@ -0,0 +1,16 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line-editor-wrapper {
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: solid 1px ${(props) => props.theme.input.border};
|
||||||
|
background-color: ${(props) => props.theme.input.bg};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { useTheme } from 'providers/Theme';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
|
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const BearerAuth = ({ item, collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const bearerToken = item.draft
|
||||||
|
? get(item, 'draft.request.auth.bearer.token')
|
||||||
|
: get(item, 'request.auth.bearer.token');
|
||||||
|
|
||||||
|
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
|
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
|
||||||
|
const handleTokenChange = (token) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'bearer',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
token: token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Token</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={bearerToken}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleTokenChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BearerAuth;
|
@ -0,0 +1,5 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div``;
|
||||||
|
|
||||||
|
export default Wrapper;
|
31
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
31
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import AuthMode from './AuthMode';
|
||||||
|
import BearerAuth from './BearerAuth';
|
||||||
|
import BasicAuth from './BasicAuth';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const Auth = ({ item, collection }) => {
|
||||||
|
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||||
|
|
||||||
|
const getAuthView = () => {
|
||||||
|
switch (authMode) {
|
||||||
|
case 'basic': {
|
||||||
|
return <BasicAuth collection={collection} item={item} />;
|
||||||
|
}
|
||||||
|
case 'bearer': {
|
||||||
|
return <BearerAuth collection={collection} item={item} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="w-full">
|
||||||
|
<div className="flex flex-grow justify-start items-center">
|
||||||
|
<AuthMode item={item} collection={collection} />
|
||||||
|
</div>
|
||||||
|
{getAuthView()}
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Auth;
|
@ -116,10 +116,11 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={param.enabled}
|
checked={param.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveParams(param)}>
|
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
|||||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||||
import RequestBody from 'components/RequestPane/RequestBody';
|
import RequestBody from 'components/RequestPane/RequestBody';
|
||||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
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 Vars from 'components/RequestPane/Vars';
|
||||||
import Assertions from 'components/RequestPane/Assertions';
|
import Assertions from 'components/RequestPane/Assertions';
|
||||||
import Script from 'components/RequestPane/Script';
|
import Script from 'components/RequestPane/Script';
|
||||||
@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
|||||||
case 'headers': {
|
case 'headers': {
|
||||||
return <RequestHeaders item={item} collection={collection} />;
|
return <RequestHeaders item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
|
case 'auth': {
|
||||||
|
return <Auth item={item} collection={collection} />;
|
||||||
|
}
|
||||||
case 'vars': {
|
case 'vars': {
|
||||||
return <Vars item={item} collection={collection} />;
|
return <Vars item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
|||||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||||
Headers
|
Headers
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||||
|
Auth
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||||
Vars
|
Vars
|
||||||
</div>
|
</div>
|
||||||
@ -95,15 +103,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
|||||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||||
Tests
|
Tests
|
||||||
</div>
|
</div>
|
||||||
{/* Moved to post mvp */}
|
|
||||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
|
||||||
{focusedTab.requestPaneTab === 'body' ? (
|
{focusedTab.requestPaneTab === 'body' ? (
|
||||||
<div className="flex flex-grow justify-end items-center">
|
<div className="flex flex-grow justify-end items-center">
|
||||||
<RequestBodyMode item={item} collection={collection} />
|
<RequestBodyMode item={item} collection={collection} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
|
<section
|
||||||
|
className={`flex w-full ${['script', 'vars', 'auth'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}
|
||||||
|
>
|
||||||
{getTabPanel(focusedTab.requestPaneTab)}
|
{getTabPanel(focusedTab.requestPaneTab)}
|
||||||
</section>
|
</section>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={param.enabled}
|
checked={param.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveParams(param)}>
|
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={param.enabled}
|
checked={param.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveParam(param)}>
|
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
|
|||||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import SingleLineEditor from 'components/SingleLineEditor';
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||||
|
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||||
|
|
||||||
const RequestHeaders = ({ item, collection }) => {
|
const RequestHeaders = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -91,6 +93,7 @@ const RequestHeaders = ({ item, collection }) => {
|
|||||||
'name'
|
'name'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
autocomplete={headerAutoCompleteList}
|
||||||
onRun={handleRun}
|
onRun={handleRun}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
@ -120,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={header.enabled}
|
checked={header.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveHeader(header)}>
|
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,10 +128,11 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={_var.enabled}
|
checked={_var.enabled}
|
||||||
|
tabIndex="-1"
|
||||||
className="mr-3 mousetrap"
|
className="mr-3 mousetrap"
|
||||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => handleRemoveVar(_var)}>
|
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||||
<IconTrash strokeWidth={1.5} size={20} />
|
<IconTrash strokeWidth={1.5} size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,13 +13,11 @@ const Placeholder = () => {
|
|||||||
<div className="px-1 py-2">Send Request</div>
|
<div className="px-1 py-2">Send Request</div>
|
||||||
<div className="px-1 py-2">New Request</div>
|
<div className="px-1 py-2">New Request</div>
|
||||||
<div className="px-1 py-2">Edit Environments</div>
|
<div className="px-1 py-2">Edit Environments</div>
|
||||||
<div className="px-1 py-2">Help</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col px-1">
|
<div className="flex flex-1 flex-col px-1">
|
||||||
<div className="px-1 py-2">Cmd + Enter</div>
|
<div className="px-1 py-2">Cmd + Enter</div>
|
||||||
<div className="px-1 py-2">Cmd + B</div>
|
<div className="px-1 py-2">Cmd + B</div>
|
||||||
<div className="px-1 py-2">Cmd + E</div>
|
<div className="px-1 py-2">Cmd + E</div>
|
||||||
<div className="px-1 py-2">Cmd + H</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: 1.25rem calc(100% - 1.25rem);
|
||||||
|
|
||||||
|
/* This is a hack to force Codemirror to use all available space */
|
||||||
|
> div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
div.CodeMirror {
|
div.CodeMirror {
|
||||||
/* todo: find a better way */
|
position: absolute;
|
||||||
height: calc(100vh - 220px);
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[role='tablist'] {
|
||||||
|
.active {
|
||||||
|
color: ${(props) => props.theme.colors.text.yellow};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -3,12 +3,57 @@ import CodeEditor from 'components/CodeEditor';
|
|||||||
import { useTheme } from 'providers/Theme';
|
import { useTheme } from 'providers/Theme';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||||
|
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
|
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => {
|
||||||
const { storedTheme } = useTheme();
|
const { storedTheme } = useTheme();
|
||||||
|
const [tab, setTab] = useState('preview');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const contentType = getContentType(headers);
|
||||||
|
const mode = getCodeMirrorModeBasedOnContentType(contentType);
|
||||||
|
|
||||||
|
const formatResponse = (data, mode) => {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.includes('json')) {
|
||||||
|
return safeStringifyJSON(data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.includes('xml')) {
|
||||||
|
let parsed = safeParseXML(data, { collapseContent: true });
|
||||||
|
|
||||||
|
if (typeof parsed === 'string') {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeStringifyJSON(parsed, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['text', 'html'].includes(mode)) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeStringifyJSON(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// final fallback
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeStringifyJSON(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = formatResponse(data, mode);
|
||||||
|
|
||||||
const onRun = () => {
|
const onRun = () => {
|
||||||
if (disableRunEventListener) {
|
if (disableRunEventListener) {
|
||||||
@ -17,18 +62,52 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
|||||||
dispatch(sendRequest(item, collection.uid));
|
dispatch(sendRequest(item, collection.uid));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const getTabClassname = (tabName) => {
|
||||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
return classnames(`select-none ${tabName}`, {
|
||||||
<div className="h-full">
|
active: tabName === tab,
|
||||||
<CodeEditor
|
'cursor-pointer': tabName !== tab
|
||||||
collection={collection}
|
});
|
||||||
theme={storedTheme}
|
};
|
||||||
onRun={onRun}
|
|
||||||
value={value || ''}
|
const getTabs = () => {
|
||||||
mode={mode}
|
if (!mode.includes('html')) {
|
||||||
readOnly
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
|
||||||
|
Raw
|
||||||
|
</div>
|
||||||
|
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
||||||
|
Preview
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeResult = useMemo(() => {
|
||||||
|
if (tab === 'preview' && mode.includes('html')) {
|
||||||
|
// Add the Base tag to the head so content loads properly. This also needs the correct CSP settings
|
||||||
|
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent.url}">`);
|
||||||
|
return (
|
||||||
|
<webview
|
||||||
|
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||||
|
webpreferences="disableDialogs=true, javascript=yes"
|
||||||
|
className="h-full bg-white"
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value} mode={mode} readOnly />;
|
||||||
|
}, [tab, collection, storedTheme, onRun, value, mode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="px-3 w-full h-full" style={{ maxWidth: width }}>
|
||||||
|
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||||
|
{getTabs()}
|
||||||
</div>
|
</div>
|
||||||
|
{activeResult}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ const ResponseSize = ({ size }) => {
|
|||||||
if (size > 1024) {
|
if (size > 1024) {
|
||||||
// size is greater than 1kb
|
// size is greater than 1kb
|
||||||
let kb = Math.floor(size / 1024);
|
let kb = Math.floor(size / 1024);
|
||||||
let decimal = ((size % 1024) / 1024).toFixed(2) * 100;
|
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
|
||||||
sizeToDisplay = kb + '.' + decimal + 'KB';
|
sizeToDisplay = kb + '.' + decimal + 'KB';
|
||||||
} else {
|
} else {
|
||||||
sizeToDisplay = size + 'B';
|
sizeToDisplay = size + 'B';
|
||||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { getContentType, formatResponse } from 'utils/common';
|
|
||||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import QueryResult from './QueryResult';
|
import QueryResult from './QueryResult';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
item={item}
|
item={item}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
width={rightPaneWidth}
|
width={rightPaneWidth}
|
||||||
value={response.data ? formatResponse(response) : ''}
|
data={response.data}
|
||||||
mode={getContentType(response.headers)}
|
headers={response.headers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -93,10 +92,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isJson = (headers) => {
|
|
||||||
return getContentType(headers) === 'application/ld+json';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex flex-col h-full relative">
|
<StyledWrapper className="flex flex-col h-full relative">
|
||||||
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
|
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
|
||||||
@ -120,7 +115,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<section className="flex flex-grow mt-5">{getTabPanel(focusedTab.responsePaneTab)}</section>
|
<section className="flex flex-grow">{getTabPanel(focusedTab.responsePaneTab)}</section>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
width={rightPaneWidth}
|
width={rightPaneWidth}
|
||||||
disableRunEventListener={true}
|
disableRunEventListener={true}
|
||||||
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
|
data={responseReceived.data}
|
||||||
|
headers={responseReceived.headers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import CodeEditor from 'components/CodeEditor/index';
|
||||||
|
import { HTTPSnippet } from 'httpsnippet';
|
||||||
|
import { useTheme } from 'providers/Theme/index';
|
||||||
|
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||||
|
|
||||||
|
const CodeView = ({ language, item }) => {
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
const { target, client, language: lang } = language;
|
||||||
|
let snippet = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
snippet = 'Error generating code snippet';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CodeEditor readOnly value={snippet} theme={storedTheme} mode={lang} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeView;
|
@ -0,0 +1,38 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
margin-inline: -1rem;
|
||||||
|
margin-block: -1.5rem;
|
||||||
|
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||||
|
|
||||||
|
.generate-code-sidebar {
|
||||||
|
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||||
|
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-code-item {
|
||||||
|
min-width: 150px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-left: solid 2px transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||||
|
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
@ -0,0 +1,145 @@
|
|||||||
|
import Modal from 'components/Modal/index';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import CodeView from './CodeView';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { isValidUrl } from 'utils/url/index';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
import { findEnvironmentInCollection } from 'utils/collections';
|
||||||
|
|
||||||
|
const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
|
||||||
|
if (!url || !url.length || typeof url !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = handlebars.compile(url, { noEscape: true });
|
||||||
|
|
||||||
|
return template({
|
||||||
|
...envVars,
|
||||||
|
...collectionVariables,
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
...processEnvVars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{
|
||||||
|
name: 'HTTP',
|
||||||
|
target: 'http',
|
||||||
|
client: 'http1.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'JavaScript-Fetch',
|
||||||
|
target: 'javascript',
|
||||||
|
client: 'fetch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Javascript-jQuery',
|
||||||
|
target: 'javascript',
|
||||||
|
client: 'jquery'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Javascript-axios',
|
||||||
|
target: 'javascript',
|
||||||
|
client: 'axios'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Python-Python3',
|
||||||
|
target: 'python',
|
||||||
|
client: 'python3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Python-Requests',
|
||||||
|
target: 'python',
|
||||||
|
client: 'requests'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PHP',
|
||||||
|
target: 'php',
|
||||||
|
client: 'curl'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Shell-curl',
|
||||||
|
target: 'shell',
|
||||||
|
client: 'curl'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Shell-httpie',
|
||||||
|
target: 'shell',
|
||||||
|
client: 'httpie'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||||
|
const url = get(item, 'request.url') || '';
|
||||||
|
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||||
|
|
||||||
|
let envVars = {};
|
||||||
|
if (environment) {
|
||||||
|
const vars = get(environment, 'variables', []);
|
||||||
|
envVars = vars.reduce((acc, curr) => {
|
||||||
|
acc[curr.name] = curr.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const interpolatedUrl = interpolateUrl({
|
||||||
|
url,
|
||||||
|
envVars,
|
||||||
|
collectionVariables: collection.collectionVariables,
|
||||||
|
processEnvVars: collection.processEnvVariables
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||||
|
return (
|
||||||
|
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||||
|
<StyledWrapper>
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div>
|
||||||
|
<div className="generate-code-sidebar">
|
||||||
|
{languages &&
|
||||||
|
languages.length &&
|
||||||
|
languages.map((language) => (
|
||||||
|
<div
|
||||||
|
key={language.name}
|
||||||
|
className={
|
||||||
|
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
|
||||||
|
}
|
||||||
|
onClick={() => setSelectedLanguage(language)}
|
||||||
|
>
|
||||||
|
<span className="capitalize">{language.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow p-4">
|
||||||
|
{isValidUrl(interpolatedUrl) ? (
|
||||||
|
<CodeView
|
||||||
|
language={selectedLanguage}
|
||||||
|
item={{
|
||||||
|
...item,
|
||||||
|
request: {
|
||||||
|
...item.request,
|
||||||
|
url: interpolatedUrl
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col justify-center items-center w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold">Invalid URL: {interpolatedUrl}</h1>
|
||||||
|
<p className="text-gray-500">Please check the URL and try again</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenerateCodeItem;
|
@ -16,6 +16,7 @@ import RenameCollectionItem from './RenameCollectionItem';
|
|||||||
import CloneCollectionItem from './CloneCollectionItem';
|
import CloneCollectionItem from './CloneCollectionItem';
|
||||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||||
import RunCollectionItem from './RunCollectionItem';
|
import RunCollectionItem from './RunCollectionItem';
|
||||||
|
import GenerateCodeItem from './GenerateCodeItem';
|
||||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||||
@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||||
|
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||||
@ -113,6 +115,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = (event) => {
|
||||||
|
setRenameItemModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
let indents = range(item.depth);
|
let indents = range(item.depth);
|
||||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||||
const isFolder = isItemAFolder(item);
|
const isFolder = isItemAFolder(item);
|
||||||
@ -166,6 +172,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
{runCollectionModalOpen && (
|
{runCollectionModalOpen && (
|
||||||
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||||
)}
|
)}
|
||||||
|
{generateCodeItemModalOpen && (
|
||||||
|
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||||
|
)}
|
||||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||||
<div className="flex items-center h-full w-full">
|
<div className="flex items-center h-full w-full">
|
||||||
{indents && indents.length
|
{indents && indents.length
|
||||||
@ -173,6 +182,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
className="indent-block"
|
className="indent-block"
|
||||||
key={i}
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
@ -188,6 +198,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
: null}
|
: null}
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
className="flex flex-grow items-center h-full overflow-hidden"
|
className="flex flex-grow items-center h-full overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: 8
|
paddingLeft: 8
|
||||||
@ -264,6 +275,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
Clone
|
Clone
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isFolder && item.type === 'http-request' && (
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdownTippyRef.current.hide();
|
||||||
|
setGenerateCodeItemModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Generate Code
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="dropdown-item delete-item"
|
className="dropdown-item delete-item"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem';
|
|||||||
import RemoveCollection from './RemoveCollection';
|
import RemoveCollection from './RemoveCollection';
|
||||||
import CollectionProperties from './CollectionProperties';
|
import CollectionProperties from './CollectionProperties';
|
||||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
|
||||||
import exportCollection from 'utils/collections/export';
|
import exportCollection from 'utils/collections/export';
|
||||||
|
|
||||||
import RenameCollection from './RenameCollection';
|
import RenameCollection from './RenameCollection';
|
||||||
@ -69,7 +69,7 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
|
|
||||||
const handleExportClick = () => {
|
const handleExportClick = () => {
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [{ isOver }, drop] = useDrop({
|
const [{ isOver }, drop] = useDrop({
|
||||||
|
@ -1,21 +1,61 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
import {
|
||||||
|
IconSearch,
|
||||||
|
IconFolders,
|
||||||
|
IconArrowsSort,
|
||||||
|
IconSortAscendingLetters,
|
||||||
|
IconSortDescendingLetters
|
||||||
|
} from '@tabler/icons';
|
||||||
import Collection from '../Collections/Collection';
|
import Collection from '../Collections/Collection';
|
||||||
import CreateCollection from '../CreateCollection';
|
import CreateCollection from '../CreateCollection';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
|
||||||
|
// todo: move this to a separate folder
|
||||||
|
// the coding convention is to keep all the components in a folder named after the component
|
||||||
const CollectionsBadge = () => {
|
const CollectionsBadge = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { collections } = useSelector((state) => state.collections);
|
||||||
|
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||||
|
const sortCollectionOrder = () => {
|
||||||
|
let order;
|
||||||
|
switch (collectionSortOrder) {
|
||||||
|
case 'default':
|
||||||
|
order = 'alphabetical';
|
||||||
|
break;
|
||||||
|
case 'alphabetical':
|
||||||
|
order = 'reverseAlphabetical';
|
||||||
|
break;
|
||||||
|
case 'reverseAlphabetical':
|
||||||
|
order = 'default';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dispatch(sortCollections({ order }));
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="items-center mt-2 relative">
|
<div className="items-center mt-2 relative">
|
||||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
<div className="collections-badge flex items-center justify-between px-2">
|
||||||
<span className="mr-2">
|
<div className="flex items-center py-1 select-none">
|
||||||
<IconFolders size={18} strokeWidth={1.5} />
|
<span className="mr-2">
|
||||||
</span>
|
<IconFolders size={18} strokeWidth={1.5} />
|
||||||
<span>Collections</span>
|
</span>
|
||||||
|
<span>Collections</span>
|
||||||
|
</div>
|
||||||
|
{collections.length >= 1 && (
|
||||||
|
<button onClick={() => sortCollectionOrder()}>
|
||||||
|
{collectionSortOrder == 'default' ? (
|
||||||
|
<IconArrowsSort size={18} strokeWidth={1.5} />
|
||||||
|
) : collectionSortOrder == 'alphabetical' ? (
|
||||||
|
<IconSortAscendingLetters size={18} strokeWidth={1.5} />
|
||||||
|
) : (
|
||||||
|
<IconSortDescendingLetters size={18} strokeWidth={1.5} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -27,8 +27,9 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
collectionFolderName: Yup.string()
|
collectionFolderName: Yup.string()
|
||||||
.min(1, 'must be atleast 1 characters')
|
.min(1, 'must be atleast 1 characters')
|
||||||
.max(50, 'must be 50 characters or less')
|
.max(50, 'must be 50 characters or less')
|
||||||
|
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||||
.required('folder name is required'),
|
.required('folder name is required'),
|
||||||
collectionLocation: Yup.string().required('location is required')
|
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||||
@ -43,7 +44,10 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
const browse = () => {
|
const browse = () => {
|
||||||
dispatch(browseDirectory())
|
dispatch(browseDirectory())
|
||||||
.then((dirPath) => {
|
.then((dirPath) => {
|
||||||
formik.setFieldValue('collectionLocation', dirPath);
|
// When the user closes the diolog without selecting anything dirPath will be false
|
||||||
|
if (typeof dirPath === 'string') {
|
||||||
|
formik.setFieldValue('collectionLocation', dirPath);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
formik.setFieldValue('collectionLocation', '');
|
formik.setFieldValue('collectionLocation', '');
|
||||||
@ -63,9 +67,8 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="collectionName" className="flex items-center">
|
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||||
<span className="font-semibold">Name</span>
|
Name
|
||||||
<Tooltip text="Name of the collection" tooltipId="collection-name" />
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="collection-name"
|
id="collection-name"
|
||||||
@ -84,9 +87,37 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
<div className="text-red-500">{formik.errors.collectionName}</div>
|
<div className="text-red-500">{formik.errors.collectionName}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
|
<label htmlFor="collection-location" className="block font-semibold mt-3">
|
||||||
|
Location
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="collection-location"
|
||||||
|
type="text"
|
||||||
|
name="collectionLocation"
|
||||||
|
readOnly={true}
|
||||||
|
className="block textbox mt-2 w-full cursor-pointer"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
value={formik.values.collectionLocation || ''}
|
||||||
|
onClick={browse}
|
||||||
|
/>
|
||||||
|
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||||
|
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||||
|
) : null}
|
||||||
|
<div className="mt-1">
|
||||||
|
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||||
|
Browse
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||||
<span className="font-semibold">Folder Name</span>
|
<span className="font-semibold">Folder Name</span>
|
||||||
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name" />
|
<Tooltip
|
||||||
|
text="This folder will be created under the selected location"
|
||||||
|
tooltipId="collection-folder-name-tooltip"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="collection-folder-name"
|
id="collection-folder-name"
|
||||||
@ -103,34 +134,6 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<>
|
|
||||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
|
||||||
Location
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="collection-location"
|
|
||||||
type="text"
|
|
||||||
name="collectionLocation"
|
|
||||||
readOnly={true}
|
|
||||||
className="block textbox mt-2 w-full"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
spellCheck="false"
|
|
||||||
value={formik.values.collectionLocation || ''}
|
|
||||||
onClick={browse}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
|
||||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="mt-1">
|
|
||||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
|
||||||
Browse
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -116,7 +116,7 @@ const Sidebar = () => {
|
|||||||
</GitHubButton>
|
</GitHubButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.3</div>
|
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.18.0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
|
|||||||
|
|
||||||
if (!SERVER_RENDERED) {
|
if (!SERVER_RENDERED) {
|
||||||
CodeMirror = require('codemirror');
|
CodeMirror = require('codemirror');
|
||||||
|
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
|
||||||
|
const word = /[\w$-]+/;
|
||||||
|
const wordlist = (options && options.autocomplete) || [];
|
||||||
|
let cur = editor.getCursor(),
|
||||||
|
curLine = editor.getLine(cur.line);
|
||||||
|
let end = cur.ch,
|
||||||
|
start = end;
|
||||||
|
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||||
|
let curWord = start != end && curLine.slice(start, end);
|
||||||
|
|
||||||
|
// Check if curWord is a valid string before proceeding
|
||||||
|
if (typeof curWord !== 'string' || curWord.length < 3) {
|
||||||
|
return null; // Abort the hint
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = (options && options.list) || [];
|
||||||
|
const re = new RegExp(word.source, 'g');
|
||||||
|
for (let dir = -1; dir <= 1; dir += 2) {
|
||||||
|
let line = cur.line,
|
||||||
|
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
|
||||||
|
for (; line != endLine; line += dir) {
|
||||||
|
let text = editor.getLine(line),
|
||||||
|
m;
|
||||||
|
while ((m = re.exec(text))) {
|
||||||
|
if (line == cur.line && curWord.length < 3) continue;
|
||||||
|
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
|
||||||
|
});
|
||||||
|
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||||
|
cm.showHint({ hint, ...options });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleLineEditor extends Component {
|
class SingleLineEditor extends Component {
|
||||||
@ -32,6 +66,7 @@ class SingleLineEditor extends Component {
|
|||||||
variables: getAllVariables(this.props.collection)
|
variables: getAllVariables(this.props.collection)
|
||||||
},
|
},
|
||||||
scrollbarStyle: null,
|
scrollbarStyle: null,
|
||||||
|
tabindex: 0,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
Enter: () => {
|
Enter: () => {
|
||||||
if (this.props.onRun) {
|
if (this.props.onRun) {
|
||||||
@ -70,9 +105,19 @@ class SingleLineEditor extends Component {
|
|||||||
},
|
},
|
||||||
'Cmd-F': () => {},
|
'Cmd-F': () => {},
|
||||||
'Ctrl-F': () => {},
|
'Ctrl-F': () => {},
|
||||||
Tab: () => {}
|
// Tabbing disabled to make tabindex work
|
||||||
|
Tab: false,
|
||||||
|
'Shift-Tab': false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (this.props.autocomplete) {
|
||||||
|
this.editor.on('keyup', (cm, event) => {
|
||||||
|
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
|
||||||
|
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||||
|
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
this.editor.setValue(this.props.value || '');
|
this.editor.setValue(this.props.value || '');
|
||||||
this.editor.on('change', this._onEdit);
|
this.editor.on('change', this._onEdit);
|
||||||
this.addOverlay();
|
this.addOverlay();
|
||||||
|
@ -87,7 +87,7 @@ const VariablesEditor = ({ collection }) => {
|
|||||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||||
|
|
||||||
<div className="mt-8 muted text-xs">
|
<div className="mt-8 muted text-xs">
|
||||||
Note: As of today, collection variables can only be set via the api -{' '}
|
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 />
|
<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.
|
In the next release, we will add a UI to set and modify collection variables.
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +54,7 @@ const Welcome = () => {
|
|||||||
<Bruno width={50} />
|
<Bruno width={50} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-semibold select-none">bruno</div>
|
<div className="text-xl font-semibold select-none">bruno</div>
|
||||||
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
|
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
|
||||||
|
|
||||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||||
<div className="mt-4 flex items-center collection-options select-none">
|
<div className="mt-4 flex items-center collection-options select-none">
|
||||||
|
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
// Add a global error event listener to capture client-side errors
|
||||||
|
window.onerror = (message, source, lineno, colno, error) => {
|
||||||
|
this.setState({ hasError: true, error });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
console.log({ error, errorInfo });
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-10">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-4 w-full">
|
||||||
|
<h1 className="text-2xl font-semibold text-red-600 mb-2">Oops! Something went wrong</h1>
|
||||||
|
<p className="text-red-600 mb-2">{this.state.error && this.state.error.toString()}</p>
|
||||||
|
{this.state.error && this.state.error.stack && (
|
||||||
|
<pre className="bg-gray-100 p-2 rounded-lg overflow-auto">{this.state.error.stack}</pre>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition"
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
|
|||||||
|
|
||||||
import ReduxStore from 'providers/ReduxStore';
|
import ReduxStore from 'providers/ReduxStore';
|
||||||
import ThemeProvider from 'providers/Theme/index';
|
import ThemeProvider from 'providers/Theme/index';
|
||||||
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
|
|
||||||
import '../styles/app.scss';
|
import '../styles/app.scss';
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeHydrate>
|
<ErrorBoundary>
|
||||||
<NoSsr>
|
<SafeHydrate>
|
||||||
<Provider store={ReduxStore}>
|
<NoSsr>
|
||||||
<ThemeProvider>
|
<Provider store={ReduxStore}>
|
||||||
<ToastProvider>
|
<ThemeProvider>
|
||||||
<AppProvider>
|
<ToastProvider>
|
||||||
<PreferencesProvider>
|
<AppProvider>
|
||||||
<HotkeysProvider>
|
<PreferencesProvider>
|
||||||
<Component {...pageProps} />
|
<HotkeysProvider>
|
||||||
</HotkeysProvider>
|
<Component {...pageProps} />
|
||||||
</PreferencesProvider>
|
</HotkeysProvider>
|
||||||
</AppProvider>
|
</PreferencesProvider>
|
||||||
</ToastProvider>
|
</AppProvider>
|
||||||
</ThemeProvider>
|
</ToastProvider>
|
||||||
</Provider>
|
</ThemeProvider>
|
||||||
</NoSsr>
|
</Provider>
|
||||||
</SafeHydrate>
|
</NoSsr>
|
||||||
|
</SafeHydrate>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
|||||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||||
import NewRequest from 'components/Sidebar/NewRequest';
|
import NewRequest from 'components/Sidebar/NewRequest';
|
||||||
import BrunoSupport from 'components/BrunoSupport';
|
|
||||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||||
@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
||||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||||
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
|
|
||||||
|
|
||||||
const getCurrentCollectionItems = () => {
|
const getCurrentCollectionItems = () => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
||||||
|
|
||||||
// help (ctrl/cmd + h)
|
|
||||||
useEffect(() => {
|
|
||||||
Mousetrap.bind(['command+h', 'ctrl+h'], (e) => {
|
|
||||||
setShowBrunoSupportModal(true);
|
|
||||||
return false; // this stops the event bubbling
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
Mousetrap.unbind(['command+h', 'ctrl+h']);
|
|
||||||
};
|
|
||||||
}, [setShowNewRequestModal]);
|
|
||||||
|
|
||||||
// close tab hotkey
|
// close tab hotkey
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||||
@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HotkeysContext.Provider {...props} value="hotkey">
|
<HotkeysContext.Provider {...props} value="hotkey">
|
||||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
|
||||||
{showSaveRequestModal && (
|
{showSaveRequestModal && (
|
||||||
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
||||||
)}
|
)}
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
getItemsToResequence,
|
getItemsToResequence,
|
||||||
moveCollectionItemToRootOfCollection,
|
moveCollectionItemToRootOfCollection,
|
||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
recursivelyGetAllItemUids,
|
|
||||||
transformRequestToSaveToFilesystem,
|
transformRequestToSaveToFilesystem,
|
||||||
findParentItemInCollection,
|
findParentItemInCollection,
|
||||||
findEnvironmentInCollection,
|
findEnvironmentInCollection,
|
||||||
@ -22,7 +21,7 @@ import {
|
|||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||||
import { waitForNextTick } from 'utils/common';
|
import { waitForNextTick } from 'utils/common';
|
||||||
import { getDirectoryName } from 'utils/common/platform';
|
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
||||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -39,6 +38,7 @@ import {
|
|||||||
createCollection as _createCollection,
|
createCollection as _createCollection,
|
||||||
renameCollection as _renameCollection,
|
renameCollection as _renameCollection,
|
||||||
removeCollection as _removeCollection,
|
removeCollection as _removeCollection,
|
||||||
|
sortCollections as _sortCollections,
|
||||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
@ -145,6 +145,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
|||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// todo: this can be directly put inside the collections/index.js file
|
||||||
|
// the coding convention is to put only actions that need ipc in this file
|
||||||
|
export const sortCollections = (order) => (dispatch) => {
|
||||||
|
dispatch(_sortCollections(order));
|
||||||
|
};
|
||||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -262,7 +267,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
}
|
}
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
ipcRenderer
|
||||||
|
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||||
|
.then(() => {
|
||||||
|
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
||||||
|
// But in windows we don't get those events, so we need to update the state manually
|
||||||
|
// This looks like an issue in our watcher library chokidar
|
||||||
|
// GH: https://github.com/usebruno/bruno/issues/251
|
||||||
|
if (isWindowsOS()) {
|
||||||
|
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -346,7 +363,16 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
|
|
||||||
ipcRenderer
|
ipcRenderer
|
||||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||||
.then(() => resolve())
|
.then(() => {
|
||||||
|
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
||||||
|
// But in windows we don't get those events, so we need to update the state manually
|
||||||
|
// This looks like an issue in our watcher library chokidar
|
||||||
|
// GH: https://github.com/usebruno/bruno/issues/265
|
||||||
|
if (isWindowsOS()) {
|
||||||
|
dispatch(_deleteItem({ itemUid, collectionUid }));
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
.catch((error) => reject(error));
|
.catch((error) => reject(error));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
|
|||||||
const PATH_SEPARATOR = path.sep;
|
const PATH_SEPARATOR = path.sep;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
collections: []
|
collections: [],
|
||||||
|
collectionSortOrder: 'default'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collectionsSlice = createSlice({
|
export const collectionsSlice = createSlice({
|
||||||
@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({
|
|||||||
createCollection: (state, action) => {
|
createCollection: (state, action) => {
|
||||||
const collectionUids = map(state.collections, (c) => c.uid);
|
const collectionUids = map(state.collections, (c) => c.uid);
|
||||||
const collection = action.payload;
|
const collection = action.payload;
|
||||||
|
|
||||||
// last action is used to track the last action performed on the collection
|
// last action is used to track the last action performed on the collection
|
||||||
// this is optional
|
// this is optional
|
||||||
// this is used in scenarios where we want to know the last action performed on the collection
|
// this is used in scenarios where we want to know the last action performed on the collection
|
||||||
// and take some extra action based on that
|
// and take some extra action based on that
|
||||||
// for example, when a env is created, we want to auto select it the env modal
|
// for example, when a env is created, we want to auto select it the env modal
|
||||||
|
collection.importedAt = new Date().getTime();
|
||||||
collection.lastAction = null;
|
collection.lastAction = null;
|
||||||
|
|
||||||
collapseCollection(collection);
|
collapseCollection(collection);
|
||||||
@ -70,6 +71,20 @@ export const collectionsSlice = createSlice({
|
|||||||
removeCollection: (state, action) => {
|
removeCollection: (state, action) => {
|
||||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||||
},
|
},
|
||||||
|
sortCollections: (state, action) => {
|
||||||
|
state.collectionSortOrder = action.payload.order;
|
||||||
|
switch (action.payload.order) {
|
||||||
|
case 'default':
|
||||||
|
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
|
||||||
|
break;
|
||||||
|
case 'alphabetical':
|
||||||
|
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
break;
|
||||||
|
case 'reverseAlphabetical':
|
||||||
|
state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
updateLastAction: (state, action) => {
|
updateLastAction: (state, action) => {
|
||||||
const { collectionUid, lastAction } = action.payload;
|
const { collectionUid, lastAction } = action.payload;
|
||||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
@ -307,6 +322,31 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateAuth: (state, action) => {
|
||||||
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||||
|
|
||||||
|
if (item && isItemARequest(item)) {
|
||||||
|
if (!item.draft) {
|
||||||
|
item.draft = cloneDeep(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.draft.request.auth = item.draft.request.auth || {};
|
||||||
|
switch (action.payload.mode) {
|
||||||
|
case 'bearer':
|
||||||
|
item.draft.request.auth.mode = 'bearer';
|
||||||
|
item.draft.request.auth.bearer = action.payload.content;
|
||||||
|
break;
|
||||||
|
case 'basic':
|
||||||
|
item.draft.request.auth.mode = 'basic';
|
||||||
|
item.draft.request.auth.basic = action.payload.content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
addQueryParam: (state, action) => {
|
addQueryParam: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -563,6 +603,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) => {
|
updateRequestBodyMode: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -1141,6 +1195,7 @@ export const {
|
|||||||
brunoConfigUpdateEvent,
|
brunoConfigUpdateEvent,
|
||||||
renameCollection,
|
renameCollection,
|
||||||
removeCollection,
|
removeCollection,
|
||||||
|
sortCollections,
|
||||||
updateLastAction,
|
updateLastAction,
|
||||||
collectionUnlinkEnvFileEvent,
|
collectionUnlinkEnvFileEvent,
|
||||||
saveEnvironment,
|
saveEnvironment,
|
||||||
@ -1158,6 +1213,7 @@ export const {
|
|||||||
collectionClicked,
|
collectionClicked,
|
||||||
collectionFolderClicked,
|
collectionFolderClicked,
|
||||||
requestUrlChanged,
|
requestUrlChanged,
|
||||||
|
updateAuth,
|
||||||
addQueryParam,
|
addQueryParam,
|
||||||
updateQueryParam,
|
updateQueryParam,
|
||||||
deleteQueryParam,
|
deleteQueryParam,
|
||||||
@ -1170,6 +1226,7 @@ export const {
|
|||||||
addMultipartFormParam,
|
addMultipartFormParam,
|
||||||
updateMultipartFormParam,
|
updateMultipartFormParam,
|
||||||
deleteMultipartFormParam,
|
deleteMultipartFormParam,
|
||||||
|
updateRequestAuthMode,
|
||||||
updateRequestBodyMode,
|
updateRequestBodyMode,
|
||||||
updateRequestBody,
|
updateRequestBody,
|
||||||
updateRequestGraphqlQuery,
|
updateRequestGraphqlQuery,
|
||||||
|
@ -9,13 +9,20 @@ const darkTheme = {
|
|||||||
green: 'rgb(11 178 126)',
|
green: 'rgb(11 178 126)',
|
||||||
danger: '#f06f57',
|
danger: '#f06f57',
|
||||||
muted: '#9d9d9d',
|
muted: '#9d9d9d',
|
||||||
purple: '#cd56d6'
|
purple: '#cd56d6',
|
||||||
|
yellow: '#f59e0b'
|
||||||
},
|
},
|
||||||
bg: {
|
bg: {
|
||||||
danger: '#d03544'
|
danger: '#d03544'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
input: {
|
||||||
|
bg: 'rgb(65, 65, 65)',
|
||||||
|
border: 'rgb(65, 65, 65)',
|
||||||
|
focusBorder: 'rgb(65, 65, 65)'
|
||||||
|
},
|
||||||
|
|
||||||
variables: {
|
variables: {
|
||||||
bg: 'rgb(48, 48, 49)',
|
bg: 'rgb(48, 48, 49)',
|
||||||
|
|
||||||
|
@ -9,13 +9,20 @@ const lightTheme = {
|
|||||||
green: '#047857',
|
green: '#047857',
|
||||||
danger: 'rgb(185, 28, 28)',
|
danger: 'rgb(185, 28, 28)',
|
||||||
muted: '#4b5563',
|
muted: '#4b5563',
|
||||||
purple: '#8e44ad'
|
purple: '#8e44ad',
|
||||||
|
yellow: '#d97706'
|
||||||
},
|
},
|
||||||
bg: {
|
bg: {
|
||||||
danger: '#dc3545'
|
danger: '#dc3545'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
input: {
|
||||||
|
bg: 'white',
|
||||||
|
border: '#ccc',
|
||||||
|
focusBorder: '#8b8b8b'
|
||||||
|
},
|
||||||
|
|
||||||
menubar: {
|
menubar: {
|
||||||
bg: 'rgb(44, 44, 44)'
|
bg: 'rgb(44, 44, 44)'
|
||||||
},
|
},
|
||||||
|
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
const createContentType = (mode) => {
|
||||||
|
switch (mode) {
|
||||||
|
case 'json':
|
||||||
|
return 'application/json';
|
||||||
|
case 'xml':
|
||||||
|
return 'application/xml';
|
||||||
|
case 'formUrlEncoded':
|
||||||
|
return 'application/x-www-form-urlencoded';
|
||||||
|
case 'multipartForm':
|
||||||
|
return 'multipart/form-data';
|
||||||
|
default:
|
||||||
|
return 'application/json';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHeaders = (headers, mode) => {
|
||||||
|
const contentType = createContentType(mode);
|
||||||
|
const headersArray = headers
|
||||||
|
.filter((header) => header.enabled)
|
||||||
|
.map((header) => {
|
||||||
|
return {
|
||||||
|
name: header.name,
|
||||||
|
value: header.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const headerNames = headersArray.map((header) => header.name);
|
||||||
|
if (!headerNames.includes('Content-Type')) {
|
||||||
|
return [...headersArray, { name: 'Content-Type', value: contentType }];
|
||||||
|
}
|
||||||
|
return headersArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createQuery = (queryParams = []) => {
|
||||||
|
return queryParams.map((param) => {
|
||||||
|
return {
|
||||||
|
name: param.name,
|
||||||
|
value: param.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPostData = (body) => {
|
||||||
|
const contentType = createContentType(body.mode);
|
||||||
|
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||||
|
return {
|
||||||
|
mimeType: contentType,
|
||||||
|
params: body[body.mode]
|
||||||
|
.filter((param) => param.enabled)
|
||||||
|
.map((param) => ({ name: param.name, value: param.value }))
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
mimeType: contentType,
|
||||||
|
text: body[body.mode]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildHarRequest = (request) => {
|
||||||
|
return {
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
httpVersion: 'HTTP/1.1',
|
||||||
|
cookies: [],
|
||||||
|
headers: createHeaders(request.headers, request.body.mode),
|
||||||
|
queryString: createQuery(request.params),
|
||||||
|
postData: createPostData(request.body),
|
||||||
|
headersSize: 0,
|
||||||
|
bodySize: 0
|
||||||
|
};
|
||||||
|
};
|
@ -66,8 +66,7 @@ if (!SERVER_RENDERED) {
|
|||||||
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
|
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!target.classList.contains('cm-variable-valid')) {
|
||||||
if (target.className !== 'cm-variable-valid') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
|||||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||||
|
|
||||||
if (draggedItemParent) {
|
if (draggedItemParent) {
|
||||||
|
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
|
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
|||||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||||
|
|
||||||
if (targetItemParent) {
|
if (targetItemParent) {
|
||||||
|
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
|
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||||
@ -203,7 +207,7 @@ export const getItemsToResequence = (parent, collection) => {
|
|||||||
return itemsToResequence;
|
return itemsToResequence;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
||||||
const copyHeaders = (headers) => {
|
const copyHeaders = (headers) => {
|
||||||
return map(headers, (header) => {
|
return map(headers, (header) => {
|
||||||
return {
|
return {
|
||||||
@ -281,6 +285,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
mode: get(si.draft.request, 'auth.mode', 'none'),
|
||||||
|
basic: {
|
||||||
|
username: get(si.draft.request, 'auth.basic.username', ''),
|
||||||
|
password: get(si.draft.request, 'auth.basic.password', '')
|
||||||
|
},
|
||||||
|
bearer: {
|
||||||
|
token: get(si.draft.request, 'auth.bearer.token', '')
|
||||||
|
}
|
||||||
|
},
|
||||||
script: si.draft.request.script,
|
script: si.draft.request.script,
|
||||||
vars: si.draft.request.vars,
|
vars: si.draft.request.vars,
|
||||||
assertions: si.draft.request.assertions,
|
assertions: si.draft.request.assertions,
|
||||||
@ -303,6 +317,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
mode: get(si.request, 'auth.mode', 'none'),
|
||||||
|
basic: {
|
||||||
|
username: get(si.request, 'auth.basic.username', ''),
|
||||||
|
password: get(si.request, 'auth.basic.password', '')
|
||||||
|
},
|
||||||
|
bearer: {
|
||||||
|
token: get(si.request, 'auth.bearer.token', '')
|
||||||
|
}
|
||||||
|
},
|
||||||
script: si.request.script,
|
script: si.request.script,
|
||||||
vars: si.request.vars,
|
vars: si.request.vars,
|
||||||
assertions: si.request.assertions,
|
assertions: si.request.assertions,
|
||||||
@ -351,6 +375,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
|||||||
url: _item.request.url,
|
url: _item.request.url,
|
||||||
params: [],
|
params: [],
|
||||||
headers: [],
|
headers: [],
|
||||||
|
auth: _item.request.auth,
|
||||||
body: _item.request.body,
|
body: _item.request.body,
|
||||||
script: _item.request.script,
|
script: _item.request.script,
|
||||||
vars: _item.request.vars,
|
vars: _item.request.vars,
|
||||||
@ -445,6 +470,22 @@ export const humanizeRequestBodyMode = (mode) => {
|
|||||||
return label;
|
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) => {
|
export const refreshUidsInItem = (item) => {
|
||||||
item.uid = uuid();
|
item.uid = uuid();
|
||||||
|
|
||||||
|
@ -25,10 +25,11 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
|||||||
stream.eat('}');
|
stream.eat('}');
|
||||||
let found = pathFoundInVariables(word, variables);
|
let found = pathFoundInVariables(word, variables);
|
||||||
if (found) {
|
if (found) {
|
||||||
return 'variable-valid';
|
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||||
} else {
|
} else {
|
||||||
return 'variable-invalid';
|
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||||
}
|
}
|
||||||
|
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
|
||||||
}
|
}
|
||||||
word += ch;
|
word += ch;
|
||||||
}
|
}
|
||||||
@ -41,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
|||||||
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCodeMirrorModeBasedOnContentType = (contentType) => {
|
||||||
|
if (!contentType || typeof contentType !== 'string') {
|
||||||
|
return 'application/text';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType.includes('json')) {
|
||||||
|
return 'application/ld+json';
|
||||||
|
} else if (contentType.includes('xml')) {
|
||||||
|
return 'application/xml';
|
||||||
|
} else if (contentType.includes('html')) {
|
||||||
|
return 'application/html';
|
||||||
|
} else if (contentType.includes('text')) {
|
||||||
|
return 'application/text';
|
||||||
|
} else if (contentType.includes('application/edn')) {
|
||||||
|
return 'application/xml';
|
||||||
|
} else if (mimeType.includes('yaml')) {
|
||||||
|
return 'application/yaml';
|
||||||
|
} else {
|
||||||
|
return 'application/text';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const safeParseXML = (str, options) => {
|
||||||
|
if (!str || !str.length || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return xmlFormat(str, options);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
||||||
export const normalizeFileName = (name) => {
|
export const normalizeFileName = (name) => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@ -76,18 +87,10 @@ export const getContentType = (headers) => {
|
|||||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||||
return 'application/xml';
|
return 'application/xml';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return contentType[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatResponse = (response) => {
|
|
||||||
let type = getContentType(response.headers);
|
|
||||||
if (type.includes('json')) {
|
|
||||||
return safeStringifyJSON(response.data, true);
|
|
||||||
}
|
|
||||||
if (type.includes('xml')) {
|
|
||||||
return xmlFormat(response.data, { collapseContent: true });
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import trim from 'lodash/trim';
|
import trim from 'lodash/trim';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import slash from './slash';
|
import slash from './slash';
|
||||||
|
import platform from 'platform';
|
||||||
|
|
||||||
export const isElectron = () => {
|
export const isElectron = () => {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
|
|||||||
|
|
||||||
return path.dirname(pathname);
|
return path.dirname(pathname);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isWindowsOS = () => {
|
||||||
|
const os = platform.os;
|
||||||
|
const osFamily = os.family.toLowerCase();
|
||||||
|
|
||||||
|
return osFamily.includes('windows');
|
||||||
|
};
|
||||||
|
@ -30,10 +30,23 @@ const parseGraphQL = (text) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformInsomniaRequestItem = (request) => {
|
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||||
|
// Check if the request name already exist and if so add a number suffix
|
||||||
|
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||||
|
if (otherItem.name === item.name && otherIndex < index) {
|
||||||
|
nameSuffix++;
|
||||||
|
}
|
||||||
|
return nameSuffix;
|
||||||
|
}, 0);
|
||||||
|
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||||
|
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||||
|
|
||||||
const brunoRequestItem = {
|
const brunoRequestItem = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
name: request.name,
|
name,
|
||||||
type: 'http-request',
|
type: 'http-request',
|
||||||
request: {
|
request: {
|
||||||
url: request.url,
|
url: request.url,
|
||||||
@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => {
|
|||||||
try {
|
try {
|
||||||
const insomniaExport = JSON.parse(data);
|
const insomniaExport = JSON.parse(data);
|
||||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||||
const insomniaCollection = insomniaResources.find(
|
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
|
||||||
(resource) => resource._type === 'workspace' && resource.scope === 'collection'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!insomniaCollection) {
|
if (!insomniaCollection) {
|
||||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||||
@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => {
|
|||||||
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||||
|
|
||||||
const folders = requestGroups.map((folder) => {
|
const folders = requestGroups.map((folder, index, allFolder) => {
|
||||||
|
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||||
const requests = resources.filter(
|
const requests = resources.filter(
|
||||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
name: folder.name,
|
name,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||||
};
|
};
|
||||||
|
@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => {
|
|||||||
|
|
||||||
return [str.slice(0, index), str.slice(index + 1)];
|
return [str.slice(0, index), str.slice(index + 1)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isValidUrl = (url) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -20,7 +20,7 @@ const run = async () => {
|
|||||||
.commandDir('commands')
|
.commandDir('commands')
|
||||||
.epilogue(CLI_EPILOGUE)
|
.epilogue(CLI_EPILOGUE)
|
||||||
.usage('Usage: $0 <command> [options]')
|
.usage('Usage: $0 <command> [options]')
|
||||||
.demandCommand(1, "Woof !! Let's play with some apis !!")
|
.demandCommand(1, "Woof !! Let's play with some APIs !!")
|
||||||
.help('h')
|
.help('h')
|
||||||
.alias('h', 'help');
|
.alias('h', 'help');
|
||||||
};
|
};
|
||||||
|
@ -187,7 +187,7 @@ const runSingleRequest = async function (
|
|||||||
// run assertions
|
// run assertions
|
||||||
let assertionResults = [];
|
let assertionResults = [];
|
||||||
const assertions = get(bruJson, 'request.assertions');
|
const assertions = get(bruJson, 'request.assertions');
|
||||||
if (assertions && assertions.length) {
|
if (assertions) {
|
||||||
const assertRuntime = new AssertRuntime();
|
const assertRuntime = new AssertRuntime();
|
||||||
assertionResults = assertRuntime.runAssertions(
|
assertionResults = assertRuntime.runAssertions(
|
||||||
assertions,
|
assertions,
|
||||||
@ -211,7 +211,7 @@ const runSingleRequest = async function (
|
|||||||
// run tests
|
// run tests
|
||||||
let testResults = [];
|
let testResults = [];
|
||||||
const testFile = get(bruJson, 'request.tests');
|
const testFile = get(bruJson, 'request.tests');
|
||||||
if (testFile && testFile.length) {
|
if (typeof testFile === 'string') {
|
||||||
const testRuntime = new TestRuntime();
|
const testRuntime = new TestRuntime();
|
||||||
const result = await testRuntime.runTests(
|
const result = await testRuntime.runTests(
|
||||||
testFile,
|
testFile,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "v0.16.3",
|
"version": "v0.18.0",
|
||||||
"name": "bruno",
|
"name": "bruno",
|
||||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||||
"homepage": "https://www.usebruno.com",
|
"homepage": "https://www.usebruno.com",
|
||||||
@ -30,6 +30,7 @@
|
|||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"https-proxy-agent": "^7.0.2",
|
||||||
"is-valid-path": "^0.1.1",
|
"is-valid-path": "^0.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
|
BIN
packages/bruno-electron/src/about/256x256.png
Normal file
BIN
packages/bruno-electron/src/about/256x256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
8
packages/bruno-electron/src/about/about.css
Normal file
8
packages/bruno-electron/src/about/about.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.versions {
|
||||||
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
@ -51,7 +51,8 @@ const template = [
|
|||||||
click: () =>
|
click: () =>
|
||||||
openAboutWindow({
|
openAboutWindow({
|
||||||
product_name: 'Bruno',
|
product_name: 'Bruno',
|
||||||
icon_path: join(process.cwd(), '/resources/icons/png/256x256.png'),
|
icon_path: join(__dirname, '../about/256x256.png'),
|
||||||
|
css_path: join(__dirname, '../about/about.css'),
|
||||||
homepage: 'https://www.usebruno.com/',
|
homepage: 'https://www.usebruno.com/',
|
||||||
package_json_dir: join(__dirname, '../..')
|
package_json_dir: join(__dirname, '../..')
|
||||||
})
|
})
|
||||||
|
@ -61,6 +61,7 @@ const bruToJson = (bru) => {
|
|||||||
url: _.get(json, 'http.url'),
|
url: _.get(json, 'http.url'),
|
||||||
params: _.get(json, 'query', []),
|
params: _.get(json, 'query', []),
|
||||||
headers: _.get(json, 'headers', []),
|
headers: _.get(json, 'headers', []),
|
||||||
|
auth: _.get(json, 'auth', {}),
|
||||||
body: _.get(json, 'body', {}),
|
body: _.get(json, 'body', {}),
|
||||||
script: _.get(json, 'script', {}),
|
script: _.get(json, 'script', {}),
|
||||||
vars: _.get(json, 'vars', {}),
|
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');
|
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||||
|
|
||||||
return transformedJson;
|
return transformedJson;
|
||||||
@ -104,10 +106,12 @@ const jsonToBru = (json) => {
|
|||||||
http: {
|
http: {
|
||||||
method: _.lowerCase(_.get(json, 'request.method')),
|
method: _.lowerCase(_.get(json, 'request.method')),
|
||||||
url: _.get(json, 'request.url'),
|
url: _.get(json, 'request.url'),
|
||||||
|
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||||
body: _.get(json, 'request.body.mode', 'none')
|
body: _.get(json, 'request.body.mode', 'none')
|
||||||
},
|
},
|
||||||
query: _.get(json, 'request.params', []),
|
query: _.get(json, 'request.params', []),
|
||||||
headers: _.get(json, 'request.headers', []),
|
headers: _.get(json, 'request.headers', []),
|
||||||
|
auth: _.get(json, 'request.auth', {}),
|
||||||
body: _.get(json, 'request.body', {}),
|
body: _.get(json, 'request.body', {}),
|
||||||
script: _.get(json, 'request.script', {}),
|
script: _.get(json, 'request.script', {}),
|
||||||
vars: {
|
vars: {
|
||||||
|
@ -16,9 +16,7 @@ setContentSecurityPolicy(`
|
|||||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||||
connect-src * 'unsafe-inline';
|
connect-src * 'unsafe-inline';
|
||||||
base-uri 'none';
|
|
||||||
form-action 'none';
|
form-action 'none';
|
||||||
img-src 'self' data:image/svg+xml;
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||||
@ -35,7 +33,8 @@ app.on('ready', async () => {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, 'preload.js')
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
webviewTag: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -291,7 +291,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run assertions
|
// run assertions
|
||||||
const assertions = get(request, 'assertions');
|
const assertions = get(request, 'assertions');
|
||||||
if (assertions && assertions.length) {
|
if (assertions) {
|
||||||
const assertRuntime = new AssertRuntime();
|
const assertRuntime = new AssertRuntime();
|
||||||
const results = assertRuntime.runAssertions(
|
const results = assertRuntime.runAssertions(
|
||||||
assertions,
|
assertions,
|
||||||
@ -313,7 +313,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run tests
|
// run tests
|
||||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||||
if (testFile && testFile.length) {
|
if (typeof testFile === 'string') {
|
||||||
const testRuntime = new TestRuntime();
|
const testRuntime = new TestRuntime();
|
||||||
const testResults = await testRuntime.runTests(
|
const testResults = await testRuntime.runTests(
|
||||||
testFile,
|
testFile,
|
||||||
@ -365,7 +365,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
if (error && error.response) {
|
if (error && error.response) {
|
||||||
// run assertions
|
// run assertions
|
||||||
const assertions = get(request, 'assertions');
|
const assertions = get(request, 'assertions');
|
||||||
if (assertions && assertions.length) {
|
if (assertions) {
|
||||||
const assertRuntime = new AssertRuntime();
|
const assertRuntime = new AssertRuntime();
|
||||||
const results = assertRuntime.runAssertions(
|
const results = assertRuntime.runAssertions(
|
||||||
assertions,
|
assertions,
|
||||||
@ -387,7 +387,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run tests
|
// run tests
|
||||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||||
if (testFile && testFile.length) {
|
if (typeof testFile === 'string') {
|
||||||
const testRuntime = new TestRuntime();
|
const testRuntime = new TestRuntime();
|
||||||
const testResults = await testRuntime.runTests(
|
const testResults = await testRuntime.runTests(
|
||||||
testFile,
|
testFile,
|
||||||
@ -702,7 +702,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run assertions
|
// run assertions
|
||||||
const assertions = get(item, 'request.assertions');
|
const assertions = get(item, 'request.assertions');
|
||||||
if (assertions && assertions.length) {
|
if (assertions) {
|
||||||
const assertRuntime = new AssertRuntime();
|
const assertRuntime = new AssertRuntime();
|
||||||
const results = assertRuntime.runAssertions(
|
const results = assertRuntime.runAssertions(
|
||||||
assertions,
|
assertions,
|
||||||
@ -723,7 +723,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run tests
|
// run tests
|
||||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||||
if (testFile && testFile.length) {
|
if (typeof testFile === 'string') {
|
||||||
const testRuntime = new TestRuntime();
|
const testRuntime = new TestRuntime();
|
||||||
const testResults = await testRuntime.runTests(
|
const testResults = await testRuntime.runTests(
|
||||||
testFile,
|
testFile,
|
||||||
@ -781,7 +781,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run assertions
|
// run assertions
|
||||||
const assertions = get(item, 'request.assertions');
|
const assertions = get(item, 'request.assertions');
|
||||||
if (assertions && assertions.length) {
|
if (assertions) {
|
||||||
const assertRuntime = new AssertRuntime();
|
const assertRuntime = new AssertRuntime();
|
||||||
const results = assertRuntime.runAssertions(
|
const results = assertRuntime.runAssertions(
|
||||||
assertions,
|
assertions,
|
||||||
@ -802,7 +802,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run tests
|
// run tests
|
||||||
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||||
if (testFile && testFile.length) {
|
if (typeof testFile === 'string') {
|
||||||
const testRuntime = new TestRuntime();
|
const testRuntime = new TestRuntime();
|
||||||
const testResults = await testRuntime.runTests(
|
const testResults = await testRuntime.runTests(
|
||||||
testFile,
|
testFile,
|
||||||
|
@ -18,6 +18,20 @@ const prepareRequest = (request) => {
|
|||||||
headers: headers
|
headers: headers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
if (request.auth) {
|
||||||
|
if (request.auth.mode === 'basic') {
|
||||||
|
axiosRequest.auth = {
|
||||||
|
username: get(request, 'auth.basic.username'),
|
||||||
|
password: get(request, 'auth.basic.password')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.auth.mode === 'bearer') {
|
||||||
|
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.body.mode === 'json') {
|
if (request.body.mode === 'json') {
|
||||||
if (!contentTypeDefined) {
|
if (!contentTypeDefined) {
|
||||||
axiosRequest.headers['content-type'] = 'application/json';
|
axiosRequest.headers['content-type'] = 'application/json';
|
||||||
|
@ -1,46 +1,48 @@
|
|||||||
const { nodeResolve } = require("@rollup/plugin-node-resolve");
|
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||||
const commonjs = require("@rollup/plugin-commonjs");
|
const commonjs = require('@rollup/plugin-commonjs');
|
||||||
const typescript = require("@rollup/plugin-typescript");
|
const typescript = require('@rollup/plugin-typescript');
|
||||||
const dts = require("rollup-plugin-dts");
|
const dts = require('rollup-plugin-dts');
|
||||||
const postcss = require("rollup-plugin-postcss");
|
const postcss = require('rollup-plugin-postcss');
|
||||||
const { terser } = require("rollup-plugin-terser");
|
const { terser } = require('rollup-plugin-terser');
|
||||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||||
|
|
||||||
const packageJson = require("./package.json");
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
input: "src/index.ts",
|
input: 'src/index.ts',
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: packageJson.main,
|
file: packageJson.main,
|
||||||
format: "cjs",
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: packageJson.module,
|
file: packageJson.module,
|
||||||
format: "esm",
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
postcss({
|
postcss({
|
||||||
minimize: true,
|
minimize: true,
|
||||||
extensions: ['.css']
|
extensions: ['.css'],
|
||||||
|
extract: true
|
||||||
}),
|
}),
|
||||||
peerDepsExternal(),
|
peerDepsExternal(),
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
extensions: ['.css']
|
extensions: ['.css']
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript({ tsconfig: "./tsconfig.json" }),
|
typescript({ tsconfig: './tsconfig.json' }),
|
||||||
terser()
|
terser()
|
||||||
],
|
],
|
||||||
external: ["react", "react-dom", "index.css"]
|
external: ['react', 'react-dom', 'index.css']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "dist/esm/index.d.ts",
|
input: 'dist/esm/index.d.ts',
|
||||||
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
external: [/\.css$/],
|
||||||
plugins: [dts.default()],
|
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
|
||||||
|
plugins: [dts.default()]
|
||||||
}
|
}
|
||||||
];
|
];
|
@ -1,6 +1,5 @@
|
|||||||
import { DocExplorer } from './components/DocExplorer';
|
import { DocExplorer } from './components/DocExplorer';
|
||||||
|
|
||||||
// Todo: Rollup throws error
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
export { DocExplorer };
|
export { DocExplorer };
|
||||||
|
@ -22,7 +22,8 @@ const { outdentString } = require('../../v1/src/utils');
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const grammar = ohm.grammar(`Bru {
|
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
|
bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||||
bodyforms = bodyformurlencoded | bodymultipart
|
bodyforms = bodyformurlencoded | bodymultipart
|
||||||
|
|
||||||
@ -75,6 +76,9 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
varsres = "vars:post-response" dictionary
|
varsres = "vars:post-response" dictionary
|
||||||
assert = "assert" assertdictionary
|
assert = "assert" assertdictionary
|
||||||
|
|
||||||
|
authbasic = "auth:basic" dictionary
|
||||||
|
authbearer = "auth:bearer" dictionary
|
||||||
|
|
||||||
body = "body" st* "{" nl* textblock tagend
|
body = "body" st* "{" nl* textblock tagend
|
||||||
bodyjson = "body:json" st* "{" nl* textblock tagend
|
bodyjson = "body:json" st* "{" nl* textblock tagend
|
||||||
bodytext = "body:text" 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
|
docs = "docs" st* "{" nl* textblock tagend
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
const mapPairListToKeyValPairs = (pairList = []) => {
|
const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||||
if (!pairList.length) {
|
if (!pairList.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return _.map(pairList[0], (pair) => {
|
return _.map(pairList[0], (pair) => {
|
||||||
let name = _.keys(pair)[0];
|
let name = _.keys(pair)[0];
|
||||||
let value = pair[name];
|
let value = pair[name];
|
||||||
|
|
||||||
|
if (!parseEnabled) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let enabled = true;
|
let enabled = true;
|
||||||
if (name && name.length && name.charAt(0) === '~') {
|
if (name && name.length && name.charAt(0) === '~') {
|
||||||
name = name.slice(1);
|
name = name.slice(1);
|
||||||
@ -282,6 +294,33 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
headers: mapPairListToKeyValPairs(dictionary.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) {
|
bodyformurlencoded(_1, dictionary) {
|
||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
|
@ -13,7 +13,7 @@ const stripLastLine = (text) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const jsonToBru = (json) => {
|
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 = '';
|
let bru = '';
|
||||||
|
|
||||||
@ -34,6 +34,11 @@ const jsonToBru = (json) => {
|
|||||||
body: ${http.body}`;
|
body: ${http.body}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (http.auth && http.auth.length) {
|
||||||
|
bru += `
|
||||||
|
auth: ${http.auth}`;
|
||||||
|
}
|
||||||
|
|
||||||
bru += `
|
bru += `
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +87,23 @@ const jsonToBru = (json) => {
|
|||||||
bru += '\n}\n\n';
|
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) {
|
if (body && body.json && body.json.length) {
|
||||||
bru += `body:json {
|
bru += `body:json {
|
||||||
${indentString(body.json)}
|
${indentString(body.json)}
|
||||||
|
@ -7,6 +7,7 @@ meta {
|
|||||||
get {
|
get {
|
||||||
url: https://api.textlocal.in/send
|
url: https://api.textlocal.in/send
|
||||||
body: json
|
body: json
|
||||||
|
auth: bearer
|
||||||
}
|
}
|
||||||
|
|
||||||
query {
|
query {
|
||||||
@ -21,6 +22,15 @@ headers {
|
|||||||
~transaction-id: {{transactionId}}
|
~transaction-id: {{transactionId}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth:basic {
|
||||||
|
username: john
|
||||||
|
password: secret
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: 123
|
||||||
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"hello": "world"
|
"hello": "world"
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"method": "get",
|
"method": "get",
|
||||||
"url": "https://api.textlocal.in/send",
|
"url": "https://api.textlocal.in/send",
|
||||||
"body": "json"
|
"body": "json",
|
||||||
|
"auth": "bearer"
|
||||||
},
|
},
|
||||||
"query": [
|
"query": [
|
||||||
{
|
{
|
||||||
@ -43,6 +44,15 @@
|
|||||||
"enabled": false
|
"enabled": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"auth": {
|
||||||
|
"basic": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "secret"
|
||||||
|
},
|
||||||
|
"bearer": {
|
||||||
|
"token": "123"
|
||||||
|
}
|
||||||
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"json": "{\n \"hello\": \"world\"\n}",
|
"json": "{\n \"hello\": \"world\"\n}",
|
||||||
"text": "This is a text body",
|
"text": "This is a text body",
|
||||||
|
@ -14,7 +14,7 @@ describe('bruToJson', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('jsonToBru', () => {
|
describe('jsonToBru', () => {
|
||||||
it('should parse the bru file', () => {
|
it('should parse the json file', () => {
|
||||||
const input = require('./fixtures/request.json');
|
const input = require('./fixtures/request.json');
|
||||||
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
|
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
|
||||||
const output = jsonToBru(input);
|
const output = jsonToBru(input);
|
||||||
|
@ -69,6 +69,27 @@ const requestBodySchema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.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
|
// 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
|
// As we introduce more request types in the future, we will improve the definition to support
|
||||||
// schema structure based on other request type
|
// schema structure based on other request type
|
||||||
@ -77,6 +98,7 @@ const requestSchema = Yup.object({
|
|||||||
method: requestMethodSchema,
|
method: requestMethodSchema,
|
||||||
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
||||||
params: Yup.array().of(keyValueSchema).required('params are required'),
|
params: Yup.array().of(keyValueSchema).required('params are required'),
|
||||||
|
auth: authSchema,
|
||||||
body: requestBodySchema,
|
body: requestBodySchema,
|
||||||
script: Yup.object({
|
script: Yup.object({
|
||||||
req: Yup.string().nullable(),
|
req: Yup.string().nullable(),
|
||||||
|
14
readme.md
14
readme.md
@ -1,7 +1,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<img src="assets/images/logo-transparent.png" width="80"/>
|
<img src="assets/images/logo-transparent.png" width="80"/>
|
||||||
|
|
||||||
### Bruno - Opensource IDE for exploring and testing api's.
|
### Bruno - Opensource IDE for exploring and testing APIs.
|
||||||
|
|
||||||
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
||||||
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||||
@ -10,36 +10,42 @@
|
|||||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
||||||
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
||||||
|
|
||||||
|
|
||||||
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
|
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
|
||||||
|
|
||||||
Bruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests.
|
Bruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests.
|
||||||
|
|
||||||
You can use git or any version control of your choice to collaborate over your api collections.
|
You can use git or any version control of your choice to collaborate over your API collections.
|
||||||
|
|
||||||
|
Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We value your data privacy and believe it should stay on your device. Read our long-term vision [here](https://github.com/usebruno/bruno/discussions/269)
|
||||||
|
|
||||||
![bruno](assets/images/landing-2.png) <br /><br />
|
![bruno](assets/images/landing-2.png) <br /><br />
|
||||||
|
|
||||||
### Run across multiple platforms 🖥️
|
### Run across multiple platforms 🖥️
|
||||||
|
|
||||||
![bruno](assets/images/run-anywhere.png) <br /><br />
|
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||||
|
|
||||||
### Collaborate via Git 👩💻🧑💻
|
### Collaborate via Git 👩💻🧑💻
|
||||||
|
|
||||||
Or any version control system of your choice
|
Or any version control system of your choice
|
||||||
|
|
||||||
![bruno](assets/images/version-control.png) <br /><br />
|
![bruno](assets/images/version-control.png) <br /><br />
|
||||||
|
|
||||||
### Website 📄
|
### Website 📄
|
||||||
|
|
||||||
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
||||||
|
|
||||||
### Documentation 📄
|
### Documentation 📄
|
||||||
|
|
||||||
Please visit [here](https://docs.usebruno.com) for documentation
|
Please visit [here](https://docs.usebruno.com) for documentation
|
||||||
|
|
||||||
### Contribute 👩💻🧑💻
|
### Contribute 👩💻🧑💻
|
||||||
|
|
||||||
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
||||||
|
|
||||||
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
||||||
|
|
||||||
### Support ❤️
|
### Support ❤️
|
||||||
|
|
||||||
Woof! If you like project, hit that ⭐ button !!
|
Woof! If you like project, hit that ⭐ button !!
|
||||||
|
|
||||||
### Authors
|
### Authors
|
||||||
@ -51,9 +57,11 @@ Woof! If you like project, hit that ⭐ button !!
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Stay in touch 🌐
|
### Stay in touch 🌐
|
||||||
|
|
||||||
[Twitter](https://twitter.com/use_bruno) <br />
|
[Twitter](https://twitter.com/use_bruno) <br />
|
||||||
[Website](https://www.usebruno.com) <br />
|
[Website](https://www.usebruno.com) <br />
|
||||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||||
|
|
||||||
### License 📄
|
### License 📄
|
||||||
|
|
||||||
[MIT](license.md)
|
[MIT](license.md)
|
||||||
|
Loading…
Reference in New Issue
Block a user