Merge branch 'main' into feature/add-bru-setNextRequest

This commit is contained in:
Martin Hoecker
2023-11-01 22:59:39 +01:00
20 changed files with 332 additions and 73 deletions

View File

@@ -42,6 +42,7 @@
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",
"parse-curl": "^0.2.6",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",

View File

@@ -146,6 +146,9 @@ export default class CodeEditor extends React.Component {
}
render() {
if (this.editor) {
this.editor.refresh();
}
return (
<StyledWrapper
className="h-full w-full"

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, forwardRef } from 'react';
import useGraphqlSchema from './useGraphqlSchema';
import { IconBook, IconDownload, IconLoader2, IconCheckmark } from '@tabler/icons';
import { IconBook, IconDownload, IconLoader2, IconRefresh } from '@tabler/icons';
import get from 'lodash/get';
import { findEnvironmentInCollection } from 'utils/collections';
import Dropdown from '../../Dropdown';
@@ -30,8 +30,8 @@ const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) =>
return (
<div ref={ref} className="dropdown-icon cursor-pointer flex hover:underline ml-2">
{isSchemaLoading && <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} />}
{!isSchemaLoading && schema && <IconDownload size={18} strokeWidth={1.5} />}
{!isSchemaLoading && !schema && <IconCheckmark size={18} strokeWidth={1.5} />}
{!isSchemaLoading && schema && <IconRefresh size={18} strokeWidth={1.5} />}
{!isSchemaLoading && !schema && <IconDownload size={18} strokeWidth={1.5} />}
<span className="ml-1">Schema</span>
</div>
);

View File

@@ -11,6 +11,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { getRequestFromCurlCommand } from 'utils/curl';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
@@ -21,7 +22,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestName: '',
requestType: 'http-request',
requestUrl: '',
requestMethod: 'GET'
requestMethod: 'GET',
curlCommand: ''
},
validationSchema: Yup.object({
requestName: Yup.string()
@@ -35,6 +37,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const trimmedValue = value ? value.trim().toLowerCase() : '';
return !['collection', 'folder'].includes(trimmedValue);
}
}),
curlCommand: Yup.string()
.min(1, 'must be at least 1 character')
.required('curlCommand is required')
.test({
name: 'curlCommand',
message: `Invalid cURL Command`,
test: (value) => getRequestFromCurlCommand(value) !== null
})
}),
onSubmit: (values) => {
@@ -61,6 +71,22 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
onClose();
})
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else if (values.requestType === 'from-curl') {
const request = getRequestFromCurlCommand(values.curlCommand);
dispatch(
newHttpRequest({
requestName: values.requestName,
requestType: 'http-request',
requestUrl: request.url,
requestMethod: request.method,
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
body: request.body
})
)
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else {
dispatch(
newHttpRequest({
@@ -124,9 +150,22 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
<input
id="from-curl"
className="cursor-pointer ml-auto"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="from-curl"
checked={formik.values.requestType === 'from-curl'}
/>
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
From cURL
</label>
</div>
</div>
<div className="mt-4">
<label htmlFor="requestName" className="block font-semibold">
Name
@@ -148,38 +187,58 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
{formik.values.requestType !== 'from-curl' ? (
<>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
/>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
/>
</div>
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
</>
) : (
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4"
style={{ resize: 'none' }}
value={formik.values.curlCommand}
onChange={formik.handleChange}
></textarea>
{formik.touched.curlCommand && formik.errors.curlCommand ? (
<div className="text-red-500">{formik.errors.curlCommand}</div>
) : null}
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
)}
</form>
</Modal>
</StyledWrapper>

View File

@@ -105,7 +105,7 @@ const Sidebar = () => {
Star
</GitHubButton>
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.27.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.0.1</div>
</div>
</div>
</div>

View File

@@ -568,7 +568,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
return new Promise((resolve, reject) => {
const state = getState();
@@ -591,9 +591,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
request: {
method: requestMethod,
url: requestUrl,
headers: [],
headers: headers ?? [],
params,
body: {
body: body ?? {
mode: 'none',
json: null,
text: null,

View File

@@ -10,7 +10,7 @@ if (!SERVER_RENDERED) {
const pathFoundInVariables = (path, obj) => {
const value = get(obj, path);
return isString(value);
return value !== undefined;
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {

View File

@@ -0,0 +1,60 @@
import * as parse from 'parse-curl';
import { BrunoError } from 'utils/common/error';
import { parseQueryParams } from 'utils/url';
export const getRequestFromCurlCommand = (command) => {
const parseFormData = (parsedBody) => {
parseQueryParams(parsedBody);
};
try {
const request = parse(command);
const parsedHeader = request?.header;
const headers =
parsedHeader && Object.keys(parsedHeader).map((key) => ({ name: key, value: parsedHeader[key], enabled: true }));
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type');
const body = {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
multipartForm: null,
formUrlEncoded: null
};
const parsedBody = request?.body;
if (parsedBody && contentType) {
switch (contentType.value.toLowerCase()) {
case 'application/json':
body.mode = 'json';
body.json = parsedBody;
break;
case 'text/xml':
body.mode = 'xml';
body.xml = parsedBody;
break;
case 'application/x-www-form-urlencoded':
body.mode = 'formUrlEncoded';
body.formUrlEncoded = parseFormData(parsedBody);
break;
case 'multipart/form-data':
body.mode = 'multipartForm';
body.multipartForm = parsedBody;
break;
case 'text/plain':
default:
body.mode = 'text';
body.text = parsedBody;
break;
}
}
return {
url: request.url,
method: request.method,
body,
headers: headers
};
} catch (error) {
return null;
}
};

View File

@@ -14,11 +14,46 @@ const readFile = (files) => {
});
};
const parseGraphQLRequest = (graphqlSource) => {
try {
let queryResultObject = {
query: '',
variables: ''
};
if (typeof graphqlSource === 'string') {
graphqlSource = JSON.parse(text);
}
if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {
queryResultObject.variables = graphqlSource.variables;
}
if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') {
queryResultObject.query = graphqlSource.query;
}
return queryResultObject;
} catch (e) {
return {
query: '',
variables: ''
};
}
};
const isItemAFolder = (item) => {
return !item.request;
};
const importPostmanV2CollectionItem = (brunoParent, item) => {
const convertV21Auth = (array) => {
return array.reduce((accumulator, currentValue) => {
accumulator[currentValue.key] = currentValue.value;
return accumulator;
}, {});
};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
brunoParent.items = brunoParent.items || [];
each(item, (i) => {
@@ -31,7 +66,7 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
};
brunoParent.items.push(brunoFolderItem);
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item);
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
}
} else {
if (i.request) {
@@ -49,6 +84,12 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
request: {
url: url,
method: i.request.method,
auth: {
mode: 'none',
basic: null,
bearer: null,
awsv4: null
},
headers: [],
params: [],
body: {
@@ -133,6 +174,12 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
}
}
if (bodyMode === 'graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(i.request.header, (header) => {
brunoRequestItem.request.headers.push({
uid: uuid(),
@@ -143,6 +190,36 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
});
});
const auth = i.request.auth ?? parentAuth;
if (auth?.[auth.type] && auth.type !== 'noauth') {
let authValues = auth[auth.type];
if (Array.isArray(authValues)) {
authValues = convertV21Auth(authValues);
}
if (auth.type === 'basic') {
brunoRequestItem.request.auth.mode = 'basic';
brunoRequestItem.request.auth.basic = {
username: authValues.username,
password: authValues.password
};
} else if (auth.type === 'bearer') {
brunoRequestItem.request.auth.mode = 'bearer';
brunoRequestItem.request.auth.bearer = {
token: authValues.token
};
} else if (auth.type === 'awsv4') {
brunoRequestItem.request.auth.mode = 'awsv4';
brunoRequestItem.request.auth.awsv4 = {
accessKeyId: authValues.accessKey,
secretAccessKey: authValues.secretKey,
sessionToken: authValues.sessionToken,
service: authValues.service,
region: authValues.region,
profileName: ''
};
}
}
each(get(i, 'request.url.query'), (param) => {
brunoRequestItem.request.params.push({
uid: uuid(),
@@ -183,7 +260,7 @@ const importPostmanV2Collection = (collection) => {
environments: []
};
importPostmanV2CollectionItem(brunoCollection, collection.item);
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
return brunoCollection;
};

View File

@@ -1,5 +1,5 @@
{
"version": "v0.27.2",
"version": "v1.0.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@@ -587,6 +587,7 @@ const registerNetworkIpc = (mainWindow) => {
scriptingConfig
);
interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars);
const axiosInstance = await configureRequest(
collection.uid,
preparedRequest,

View File

@@ -1,5 +1,6 @@
const { get, each, filter } = require('lodash');
const { get, each, filter, forOwn, extend } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
// Authentication
// A request can override the collection auth with another auth