Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Now based on the request type appropriate views are shown. #3340

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ const StyledWrapper = styled.div`
textarea.curl-command {
min-height: 150px;
}

.dropdown {
width: fit-content;

.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;

export default StyledWrapper;
101 changes: 94 additions & 7 deletions packages/bruno-app/src/components/Sidebar/NewRequest/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useCallback } from 'react';
import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
Expand All @@ -12,13 +12,60 @@ import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelect
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { getRequestFromCurlCommand } from 'utils/curl';
import Dropdown from 'components/Dropdown';
import { IconCaretDown } from '@tabler/icons';

const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const {
brunoConfig: { presets: collectionPresets = {} }
} = collection;
const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null);

const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);

const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
{curlRequestTypeDetected === 'http-request' ? "HTTP" : "GraphQL"}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});

// This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request.
const identifyCurlRequestType = (url, headers, body) => {
if (url.endsWith('/graphql')) {
setCurlRequestTypeDetected('graphql-request');
return;
}

const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
if (contentType && contentType.includes('application/graphql')) {
setCurlRequestTypeDetected('graphql-request');
return;
}

if (body.json) {
try {
const parsedJson = JSON.parse(body.json);
if (parsedJson && (parsedJson.query || parsedJson.variables || parsedJson.mutation)) {
setCurlRequestTypeDetected('graphql-request');
return;
}
} catch (err) {
console.error('Failed to parse JSON body:', err);
}
}

setCurlRequestTypeDetected('http-request');
};

const curlRequestTypeChange = (type) => {
setCurlRequestTypeDetected(type);
};

const getRequestType = (collectionPresets) => {
if (!collectionPresets || !collectionPresets.requestType) {
Expand Down Expand Up @@ -99,11 +146,11 @@ const NewRequest = ({ collection, item, isEphemeral, 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);
const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected);
dispatch(
newHttpRequest({
requestName: values.requestName,
requestType: 'http-request',
requestType: curlRequestTypeDetected,
requestUrl: request.url,
requestMethod: request.method,
collectionUid: collection.uid,
Expand Down Expand Up @@ -158,13 +205,31 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
formik.setFieldValue('requestType', 'from-curl');
formik.setFieldValue('curlCommand', pastedData);

// Identify the request type
const request = getRequestFromCurlCommand(pastedData);
if (request) {
identifyCurlRequestType(request.url, request.headers, request.body);
}

// Prevent the default paste behavior to avoid pasting into the textarea
event.preventDefault();
}
},
[formik]
);

const handleCurlCommandChange = (event) => {
formik.handleChange(event);

if (event.target.name === 'curlCommand') {
const curlCommand = event.target.value;
const request = getRequestFromCurlCommand(curlCommand);
if (request) {
identifyCurlRequestType(request.url, request.headers, request.body);
}
}
};

return (
<StyledWrapper>
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
Expand Down Expand Up @@ -279,15 +344,37 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
</>
) : (
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<div className="flex justify-between">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('http-request');
}}
>
HTTP
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('graphql-request');
}}
>
GraphQL
</div>
</Dropdown>
</div>
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4 curl-command"
value={formik.values.curlCommand}
onChange={formik.handleChange}
onChange={handleCurlCommandChange}
></textarea>
{formik.touched.curlCommand && formik.errors.curlCommand ? (
<div className="text-red-500">{formik.errors.curlCommand}</div>
Expand Down
11 changes: 9 additions & 2 deletions packages/bruno-app/src/utils/codegenerator/har.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@ const createContentType = (mode) => {
}
};

/**
* Creates a list of enabled headers for the request, ensuring no duplicate content-type headers.
*
* @param {Object} request - The request object.
* @param {Object[]} headers - The array of header objects, each containing name, value, and enabled properties.
* @returns {Object[]} - An array of enabled headers with normalized names and values.
*/
const createHeaders = (request, headers) => {
const enabledHeaders = headers
.filter((header) => header.enabled)
.map((header) => ({
name: header.name,
name: header.name.toLowerCase(),
value: header.value
}));

const contentType = createContentType(request.body?.mode);
if (contentType !== '') {
if (contentType !== '' && !enabledHeaders.some((header) => header.name === 'content-type')) {
enabledHeaders.push({ name: 'content-type', value: contentType });
}

Expand Down
14 changes: 13 additions & 1 deletion packages/bruno-app/src/utils/curl/curl-to-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ function getQueries(request) {
return queries;
}

/**
* Converts request data to a string based on its content type.
*
* @param {Object} request - The request object containing data and headers.
* @returns {Object} An object containing the data string.
*/
function getDataString(request) {
if (typeof request.data === 'number') {
request.data = request.data.toString();
Expand All @@ -44,7 +50,13 @@ function getDataString(request) {
const contentType = getContentType(request.headers);

if (contentType && contentType.includes('application/json')) {
return { data: request.data.toString() };
try {
const parsedData = JSON.parse(request.data);
return { data: JSON.stringify(parsedData) };
} catch (error) {
console.error('Failed to parse JSON data:', error);
return { data: request.data.toString() };
}
}

const parsedQueryString = querystring.parse(request.data, { sort: false });
Expand Down
29 changes: 25 additions & 4 deletions packages/bruno-app/src/utils/curl/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { forOwn } from 'lodash';
import { convertToCodeMirrorJson } from 'utils/common';
import curlToJson from './curl-to-json';

export const getRequestFromCurlCommand = (curlCommand) => {
export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {
const parseFormData = (parsedBody) => {
const formData = [];
forOwn(parsedBody, (value, key) => {
Expand All @@ -12,6 +12,22 @@ export const getRequestFromCurlCommand = (curlCommand) => {
return formData;
};

const parseGraphQL = (text) => {
try {
const graphql = JSON.parse(text);

return {
query: graphql.query,
variables: JSON.stringify(graphql.variables, null, 2)
};
} catch (e) {
return {
query: '',
variables: ''
};
}
};

try {
if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) {
return null;
Expand All @@ -24,18 +40,23 @@ export const getRequestFromCurlCommand = (curlCommand) => {
Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true }));

const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
const parsedBody = request.data;

const body = {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
multipartForm: null,
formUrlEncoded: null
graphql: null
};
const parsedBody = request.data;

if (parsedBody && contentType && typeof contentType === 'string') {
if (contentType.includes('application/json')) {
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
} else if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = convertToCodeMirrorJson(parsedBody);
} else if (contentType.includes('text/xml')) {
Expand Down