mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-22 14:41:04 +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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Local Development
|
||||
|
||||
@ -15,7 +16,6 @@ nvm use
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
|
@ -38,6 +38,5 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
@ -30,8 +30,11 @@
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
|
@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full"
|
||||
className="h-full w-full"
|
||||
aria-label="Code Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&.modal--animate-out {
|
||||
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
||||
|
@ -197,10 +197,11 @@ const AssertionRow = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assertion.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</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
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Auth from 'components/RequestPane/Auth';
|
||||
import AuthMode from 'components/RequestPane/Auth/AuthMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
@ -95,15 +103,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</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)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
|
@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParam(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -91,6 +93,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
@ -120,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveHeader(header)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -128,10 +128,11 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -13,13 +13,11 @@ const Placeholder = () => {
|
||||
<div className="px-1 py-2">Send 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">Help</div>
|
||||
</div>
|
||||
<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 + B</div>
|
||||
<div className="px-1 py-2">Cmd + E</div>
|
||||
<div className="px-1 py-2">Cmd + H</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -1,9 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
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 {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
position: absolute;
|
||||
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 { useDispatch } from 'react-redux';
|
||||
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 { 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 [tab, setTab] = useState('preview');
|
||||
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 = () => {
|
||||
if (disableRunEventListener) {
|
||||
@ -17,18 +62,52 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
onRun={onRun}
|
||||
value={value || ''}
|
||||
mode={mode}
|
||||
readOnly
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`select-none ${tabName}`, {
|
||||
active: tabName === tab,
|
||||
'cursor-pointer': tabName !== tab
|
||||
});
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
if (!mode.includes('html')) {
|
||||
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>
|
||||
{activeResult}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ const ResponseSize = ({ size }) => {
|
||||
if (size > 1024) {
|
||||
// size is greater than 1kb
|
||||
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';
|
||||
} else {
|
||||
sizeToDisplay = size + 'B';
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getContentType, formatResponse } from 'utils/common';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
import Overlay from './Overlay';
|
||||
@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? formatResponse(response) : ''}
|
||||
mode={getContentType(response.headers)}
|
||||
data={response.data}
|
||||
headers={response.headers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -93,10 +92,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isJson = (headers) => {
|
||||
return getContentType(headers) === 'application/ld+json';
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
|
||||
@ -120,7 +115,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(focusedTab.responsePaneTab)}</section>
|
||||
<section className="flex flex-grow">{getTabPanel(focusedTab.responsePaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
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 DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import GenerateCodeItem from './GenerateCodeItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = 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);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const isFolder = isItemAFolder(item);
|
||||
@ -166,6 +172,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{runCollectionModalOpen && (
|
||||
<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="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
@ -173,6 +182,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{
|
||||
@ -188,6 +198,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
: null}
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
@ -264,6 +275,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
Clone
|
||||
</div>
|
||||
)}
|
||||
{!isFolder && item.type === 'http-request' && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Generate Code
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
|
@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import CollectionProperties from './CollectionProperties';
|
||||
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 RenameCollection from './RenameCollection';
|
||||
@ -69,7 +69,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
};
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
|
@ -1,21 +1,61 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
IconSearch,
|
||||
IconFolders,
|
||||
IconArrowsSort,
|
||||
IconSortAscendingLetters,
|
||||
IconSortDescendingLetters
|
||||
} from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// 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 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 (
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
<div className="collections-badge flex items-center justify-between px-2">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
|
@ -27,8 +27,9 @@ const CreateCollection = ({ onClose }) => {
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string().required('location is required')
|
||||
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
@ -43,7 +44,10 @@ const CreateCollection = ({ onClose }) => {
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
// When the user closes the diolog without selecting anything dirPath will be false
|
||||
if (typeof dirPath === 'string') {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
@ -63,9 +67,8 @@ const CreateCollection = ({ onClose }) => {
|
||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="flex items-center">
|
||||
<span className="font-semibold">Name</span>
|
||||
<Tooltip text="Name of the collection" tooltipId="collection-name" />
|
||||
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
@ -84,9 +87,37 @@ const CreateCollection = ({ onClose }) => {
|
||||
<div className="text-red-500">{formik.errors.collectionName}</div>
|
||||
) : null}
|
||||
|
||||
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
|
||||
<label htmlFor="collection-location" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name" />
|
||||
<Tooltip
|
||||
text="This folder will be created under the selected location"
|
||||
tooltipId="collection-folder-name-tooltip"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
@ -103,34 +134,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
@ -116,7 +116,7 @@ const Sidebar = () => {
|
||||
</GitHubButton>
|
||||
)}
|
||||
</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>
|
||||
|
@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
|
||||
const word = /[\w$-]+/;
|
||||
const wordlist = (options && options.autocomplete) || [];
|
||||
let cur = editor.getCursor(),
|
||||
curLine = editor.getLine(cur.line);
|
||||
let end = cur.ch,
|
||||
start = end;
|
||||
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && curLine.slice(start, end);
|
||||
|
||||
// Check if curWord is a valid string before proceeding
|
||||
if (typeof curWord !== 'string' || curWord.length < 3) {
|
||||
return null; // Abort the hint
|
||||
}
|
||||
|
||||
const list = (options && options.list) || [];
|
||||
const re = new RegExp(word.source, 'g');
|
||||
for (let dir = -1; dir <= 1; dir += 2) {
|
||||
let line = cur.line,
|
||||
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
|
||||
for (; line != endLine; line += dir) {
|
||||
let text = editor.getLine(line),
|
||||
m;
|
||||
while ((m = re.exec(text))) {
|
||||
if (line == cur.line && curWord.length < 3) continue;
|
||||
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
|
||||
});
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
@ -32,6 +66,7 @@ class SingleLineEditor extends Component {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
@ -70,9 +105,19 @@ class SingleLineEditor extends Component {
|
||||
},
|
||||
'Cmd-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.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
|
@ -87,7 +87,7 @@ const VariablesEditor = ({ collection }) => {
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, collection variables can only be set via the api -{' '}
|
||||
Note: As of today, collection variables can only be set via the API -{' '}
|
||||
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify collection variables.
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@ const Welcome = () => {
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
|
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
44
packages/bruno-app/src/pages/ErrorBoundary/index.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
componentDidMount() {
|
||||
// Add a global error event listener to capture client-side errors
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
this.setState({ hasError: true, error });
|
||||
};
|
||||
}
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.log({ error, errorInfo });
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10">
|
||||
<div className="bg-white rounded-lg shadow-lg p-4 w-full">
|
||||
<h1 className="text-2xl font-semibold text-red-600 mb-2">Oops! Something went wrong</h1>
|
||||
<p className="text-red-600 mb-2">{this.state.error && this.state.error.toString()}</p>
|
||||
{this.state.error && this.state.error.stack && (
|
||||
<pre className="bg-gray-100 p-2 rounded-lg overflow-auto">{this.state.error.stack}</pre>
|
||||
)}
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition"
|
||||
onClick={() => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
|
||||
|
||||
import ReduxStore from 'providers/ReduxStore';
|
||||
import ThemeProvider from 'providers/Theme/index';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
import '../styles/app.scss';
|
||||
import '../styles/globals.css';
|
||||
@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<PreferencesProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</PreferencesProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
</SafeHydrate>
|
||||
<ErrorBoundary>
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<PreferencesProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</PreferencesProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
</SafeHydrate>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
|
||||
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
|
||||
|
||||
const getCurrentCollectionItems = () => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||
@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
||||
{showSaveRequestModal && (
|
||||
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
||||
)}
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
getItemsToResequence,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
findCollectionByUid,
|
||||
recursivelyGetAllItemUids,
|
||||
transformRequestToSaveToFilesystem,
|
||||
findParentItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
@ -22,7 +21,7 @@ import {
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
@ -39,6 +38,7 @@ import {
|
||||
createCollection as _createCollection,
|
||||
renameCollection as _renameCollection,
|
||||
removeCollection as _removeCollection,
|
||||
sortCollections as _sortCollections,
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||
} from './index';
|
||||
|
||||
@ -145,6 +145,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
||||
.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) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@ -262,7 +267,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
||||
ipcRenderer
|
||||
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/251
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@ -346,7 +363,16 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/265
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_deleteItem({ itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
return;
|
||||
|
@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
const initialState = {
|
||||
collections: []
|
||||
collections: [],
|
||||
collectionSortOrder: 'default'
|
||||
};
|
||||
|
||||
export const collectionsSlice = createSlice({
|
||||
@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({
|
||||
createCollection: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
// 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
|
||||
// for example, when a env is created, we want to auto select it the env modal
|
||||
collection.importedAt = new Date().getTime();
|
||||
collection.lastAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
@ -70,6 +71,20 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
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) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
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) => {
|
||||
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) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1141,6 +1195,7 @@ export const {
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
@ -1158,6 +1213,7 @@ export const {
|
||||
collectionClicked,
|
||||
collectionFolderClicked,
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
@ -1170,6 +1226,7 @@ export const {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam,
|
||||
updateRequestAuthMode,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
|
@ -9,13 +9,20 @@ const darkTheme = {
|
||||
green: 'rgb(11 178 126)',
|
||||
danger: '#f06f57',
|
||||
muted: '#9d9d9d',
|
||||
purple: '#cd56d6'
|
||||
purple: '#cd56d6',
|
||||
yellow: '#f59e0b'
|
||||
},
|
||||
bg: {
|
||||
danger: '#d03544'
|
||||
}
|
||||
},
|
||||
|
||||
input: {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
},
|
||||
|
||||
variables: {
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
|
||||
|
@ -9,13 +9,20 @@ const lightTheme = {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563',
|
||||
purple: '#8e44ad'
|
||||
purple: '#8e44ad',
|
||||
yellow: '#d97706'
|
||||
},
|
||||
bg: {
|
||||
danger: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
input: {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
},
|
||||
|
||||
menubar: {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.className !== 'cm-variable-valid') {
|
||||
if (!target.classList.contains('cm-variable-valid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
@ -203,7 +207,7 @@ export const getItemsToResequence = (parent, collection) => {
|
||||
return itemsToResequence;
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
return map(headers, (header) => {
|
||||
return {
|
||||
@ -281,6 +285,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||
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,
|
||||
vars: si.draft.request.vars,
|
||||
assertions: si.draft.request.assertions,
|
||||
@ -303,6 +317,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||
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,
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
@ -351,6 +375,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
url: _item.request.url,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: _item.request.auth,
|
||||
body: _item.request.body,
|
||||
script: _item.request.script,
|
||||
vars: _item.request.vars,
|
||||
@ -445,6 +470,22 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
return label;
|
||||
};
|
||||
|
||||
export const humanizeRequestAuthMode = (mode) => {
|
||||
let label = 'No Auth';
|
||||
switch (mode) {
|
||||
case 'basic': {
|
||||
label = 'Basic Auth';
|
||||
break;
|
||||
}
|
||||
case 'bearer': {
|
||||
label = 'Bearer Token';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
export const refreshUidsInItem = (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
|
@ -25,10 +25,11 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
stream.eat('}');
|
||||
let found = pathFoundInVariables(word, variables);
|
||||
if (found) {
|
||||
return 'variable-valid';
|
||||
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||
} else {
|
||||
return 'variable-invalid';
|
||||
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
|
||||
}
|
||||
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
|
||||
}
|
||||
word += ch;
|
||||
}
|
||||
@ -41,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
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
|
||||
export const normalizeFileName = (name) => {
|
||||
if (!name) {
|
||||
@ -76,18 +87,10 @@ export const getContentType = (headers) => {
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType[0];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const formatResponse = (response) => {
|
||||
let type = getContentType(response.headers);
|
||||
if (type.includes('json')) {
|
||||
return safeStringifyJSON(response.data, true);
|
||||
}
|
||||
if (type.includes('xml')) {
|
||||
return xmlFormat(response.data, { collapseContent: true });
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
import platform from 'platform';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
|
||||
|
||||
return path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('windows');
|
||||
};
|
||||
|
@ -30,10 +30,23 @@ const parseGraphQL = (text) => {
|
||||
}
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request) => {
|
||||
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||
// Check if the request name already exist and if so add a number suffix
|
||||
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||
if (otherItem.name === item.name && otherIndex < index) {
|
||||
nameSuffix++;
|
||||
}
|
||||
return nameSuffix;
|
||||
}, 0);
|
||||
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: request.name,
|
||||
name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => {
|
||||
try {
|
||||
const insomniaExport = JSON.parse(data);
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find(
|
||||
(resource) => resource._type === 'workspace' && resource.scope === 'collection'
|
||||
);
|
||||
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||
@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => {
|
||||
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder) => {
|
||||
const folders = requestGroups.map((folder, index, allFolder) => {
|
||||
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||
const requests = resources.filter(
|
||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||
);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: folder.name,
|
||||
name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||
};
|
||||
|
@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => {
|
||||
|
||||
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')
|
||||
.epilogue(CLI_EPILOGUE)
|
||||
.usage('Usage: $0 <command> [options]')
|
||||
.demandCommand(1, "Woof !! Let's play with some apis !!")
|
||||
.demandCommand(1, "Woof !! Let's play with some APIs !!")
|
||||
.help('h')
|
||||
.alias('h', 'help');
|
||||
};
|
||||
|
@ -187,7 +187,7 @@ const runSingleRequest = async function (
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -211,7 +211,7 @@ const runSingleRequest = async function (
|
||||
// run tests
|
||||
let testResults = [];
|
||||
const testFile = get(bruJson, 'request.tests');
|
||||
if (testFile && testFile.length) {
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const result = await testRuntime.runTests(
|
||||
testFile,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v0.16.3",
|
||||
"version": "v0.18.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@ -30,6 +30,7 @@
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"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: () =>
|
||||
openAboutWindow({
|
||||
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/',
|
||||
package_json_dir: join(__dirname, '../..')
|
||||
})
|
||||
|
@ -61,6 +61,7 @@ const bruToJson = (bru) => {
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
@ -69,6 +70,7 @@ const bruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
@ -104,10 +106,12 @@ const jsonToBru = (json) => {
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
query: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
|
@ -16,9 +16,7 @@ setContentSecurityPolicy(`
|
||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src * 'unsafe-inline';
|
||||
base-uri 'none';
|
||||
form-action 'none';
|
||||
img-src 'self' data:image/svg+xml;
|
||||
`);
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
@ -35,7 +33,8 @@ app.on('ready', async () => {
|
||||
webPreferences: {
|
||||
nodeIntegration: 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
|
||||
const assertions = get(request, 'assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -313,7 +313,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run 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 testResults = await testRuntime.runTests(
|
||||
testFile,
|
||||
@ -365,7 +365,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (error && error.response) {
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -387,7 +387,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run 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 testResults = await testRuntime.runTests(
|
||||
testFile,
|
||||
@ -702,7 +702,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -723,7 +723,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run 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 testResults = await testRuntime.runTests(
|
||||
testFile,
|
||||
@ -781,7 +781,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions && assertions.length) {
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
@ -802,7 +802,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run 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 testResults = await testRuntime.runTests(
|
||||
testFile,
|
||||
|
@ -18,6 +18,20 @@ const prepareRequest = (request) => {
|
||||
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 (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
|
@ -1,46 +1,48 @@
|
||||
const { nodeResolve } = require("@rollup/plugin-node-resolve");
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
const typescript = require("@rollup/plugin-typescript");
|
||||
const dts = require("rollup-plugin-dts");
|
||||
const postcss = require("rollup-plugin-postcss");
|
||||
const { terser } = require("rollup-plugin-terser");
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const typescript = require('@rollup/plugin-typescript');
|
||||
const dts = require('rollup-plugin-dts');
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
|
||||
const packageJson = require("./package.json");
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: "src/index.ts",
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: "cjs",
|
||||
sourcemap: true,
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: "esm",
|
||||
sourcemap: true,
|
||||
},
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
postcss({
|
||||
minimize: true,
|
||||
extensions: ['.css']
|
||||
extensions: ['.css'],
|
||||
extract: true
|
||||
}),
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.css']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: "./tsconfig.json" }),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
],
|
||||
external: ["react", "react-dom", "index.css"]
|
||||
external: ['react', 'react-dom', 'index.css']
|
||||
},
|
||||
{
|
||||
input: "dist/esm/index.d.ts",
|
||||
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
||||
plugins: [dts.default()],
|
||||
input: 'dist/esm/index.d.ts',
|
||||
external: [/\.css$/],
|
||||
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
|
||||
plugins: [dts.default()]
|
||||
}
|
||||
];
|
@ -1,6 +1,5 @@
|
||||
import { DocExplorer } from './components/DocExplorer';
|
||||
|
||||
// Todo: Rollup throws error
|
||||
import './index.css';
|
||||
|
||||
export { DocExplorer };
|
||||
|
@ -22,7 +22,8 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
*
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | query | headers | bodies | varsandassert | script | tests | docs)*
|
||||
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
auths = authbasic | authbearer
|
||||
bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
|
||||
@ -75,6 +76,9 @@ const grammar = ohm.grammar(`Bru {
|
||||
varsres = "vars:post-response" dictionary
|
||||
assert = "assert" assertdictionary
|
||||
|
||||
authbasic = "auth:basic" dictionary
|
||||
authbearer = "auth:bearer" dictionary
|
||||
|
||||
body = "body" st* "{" nl* textblock tagend
|
||||
bodyjson = "body:json" st* "{" nl* textblock tagend
|
||||
bodytext = "body:text" st* "{" nl* textblock tagend
|
||||
@ -92,13 +96,21 @@ const grammar = ohm.grammar(`Bru {
|
||||
docs = "docs" st* "{" nl* textblock tagend
|
||||
}`);
|
||||
|
||||
const mapPairListToKeyValPairs = (pairList = []) => {
|
||||
const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
if (!pairList.length) {
|
||||
return [];
|
||||
}
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
|
||||
if (!parseEnabled) {
|
||||
return {
|
||||
name,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
let enabled = true;
|
||||
if (name && name.length && name.charAt(0) === '~') {
|
||||
name = name.slice(1);
|
||||
@ -282,6 +294,33 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
headers: mapPairListToKeyValPairs(dictionary.ast)
|
||||
};
|
||||
},
|
||||
authbasic(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const usernameKey = _.find(auth, { name: 'username' });
|
||||
const passwordKey = _.find(auth, { name: 'password' });
|
||||
const username = usernameKey ? usernameKey.value : '';
|
||||
const password = passwordKey ? passwordKey.value : '';
|
||||
return {
|
||||
auth: {
|
||||
basic: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
authbearer(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const tokenKey = _.find(auth, { name: 'token' });
|
||||
const token = tokenKey ? tokenKey.value : '';
|
||||
return {
|
||||
auth: {
|
||||
bearer: {
|
||||
token
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
bodyformurlencoded(_1, dictionary) {
|
||||
return {
|
||||
body: {
|
||||
|
@ -13,7 +13,7 @@ const stripLastLine = (text) => {
|
||||
};
|
||||
|
||||
const jsonToBru = (json) => {
|
||||
const { meta, http, query, headers, body, script, tests, vars, assertions, docs } = json;
|
||||
const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json;
|
||||
|
||||
let bru = '';
|
||||
|
||||
@ -34,6 +34,11 @@ const jsonToBru = (json) => {
|
||||
body: ${http.body}`;
|
||||
}
|
||||
|
||||
if (http.auth && http.auth.length) {
|
||||
bru += `
|
||||
auth: ${http.auth}`;
|
||||
}
|
||||
|
||||
bru += `
|
||||
}
|
||||
|
||||
@ -82,6 +87,23 @@ const jsonToBru = (json) => {
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (auth && auth.basic) {
|
||||
bru += `auth:basic {
|
||||
${indentString(`username: ${auth.basic.username}`)}
|
||||
${indentString(`password: ${auth.basic.password}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (auth && auth.bearer) {
|
||||
bru += `auth:bearer {
|
||||
${indentString(`token: ${auth.bearer.token}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (body && body.json && body.json.length) {
|
||||
bru += `body:json {
|
||||
${indentString(body.json)}
|
||||
|
@ -7,6 +7,7 @@ meta {
|
||||
get {
|
||||
url: https://api.textlocal.in/send
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
query {
|
||||
@ -21,6 +22,15 @@ headers {
|
||||
~transaction-id: {{transactionId}}
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: john
|
||||
password: secret
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: 123
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "world"
|
||||
|
@ -7,7 +7,8 @@
|
||||
"http": {
|
||||
"method": "get",
|
||||
"url": "https://api.textlocal.in/send",
|
||||
"body": "json"
|
||||
"body": "json",
|
||||
"auth": "bearer"
|
||||
},
|
||||
"query": [
|
||||
{
|
||||
@ -43,6 +44,15 @@
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"basic": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
},
|
||||
"bearer": {
|
||||
"token": "123"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"json": "{\n \"hello\": \"world\"\n}",
|
||||
"text": "This is a text body",
|
||||
|
@ -14,7 +14,7 @@ describe('bruToJson', () => {
|
||||
});
|
||||
|
||||
describe('jsonToBru', () => {
|
||||
it('should parse the bru file', () => {
|
||||
it('should parse the json file', () => {
|
||||
const input = require('./fixtures/request.json');
|
||||
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
|
||||
const output = jsonToBru(input);
|
||||
|
@ -69,6 +69,27 @@ const requestBodySchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBasicSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBearerSchema = Yup.object({
|
||||
token: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authSchema = Yup.object({
|
||||
mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'),
|
||||
basic: authBasicSchema.nullable(),
|
||||
bearer: authBearerSchema.nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
// Right now, the request schema is very tightly coupled with http request
|
||||
// As we introduce more request types in the future, we will improve the definition to support
|
||||
// schema structure based on other request type
|
||||
@ -77,6 +98,7 @@ const requestSchema = Yup.object({
|
||||
method: requestMethodSchema,
|
||||
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
||||
params: Yup.array().of(keyValueSchema).required('params are required'),
|
||||
auth: authSchema,
|
||||
body: requestBodySchema,
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
|
14
readme.md
14
readme.md
@ -1,7 +1,7 @@
|
||||
<br />
|
||||
<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)
|
||||
[![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)
|
||||
[![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 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 />
|
||||
|
||||
### Run across multiple platforms 🖥️
|
||||
|
||||
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||
|
||||
### Collaborate via Git 👩💻🧑💻
|
||||
|
||||
Or any version control system of your choice
|
||||
|
||||
![bruno](assets/images/version-control.png) <br /><br />
|
||||
|
||||
### Website 📄
|
||||
|
||||
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
||||
|
||||
### Documentation 📄
|
||||
|
||||
Please visit [here](https://docs.usebruno.com) for documentation
|
||||
|
||||
### Contribute 👩💻🧑💻
|
||||
|
||||
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
||||
|
||||
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
||||
|
||||
### Support ❤️
|
||||
|
||||
Woof! If you like project, hit that ⭐ button !!
|
||||
|
||||
### Authors
|
||||
@ -51,9 +57,11 @@ Woof! If you like project, hit that ⭐ button !!
|
||||
</div>
|
||||
|
||||
### Stay in touch 🌐
|
||||
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
|
||||
### License 📄
|
||||
|
||||
[MIT](license.md)
|
||||
|
Loading…
Reference in New Issue
Block a user