mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 06:28:33 +00:00
Merge branch 'main' into feature/add-bru-setNextRequest
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
60
packages/bruno-app/src/utils/curl/index.js
Normal file
60
packages/bruno-app/src/utils/curl/index.js
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -587,6 +587,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars);
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
preparedRequest,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user