diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 4845116a..7be069c1 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -30,6 +30,7 @@ "graphiql": "^1.5.9", "graphql": "^16.6.0", "graphql-request": "^3.7.0", + "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", "know-your-http-well": "^0.5.0", diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index bcc13b5c..96d5bb48 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component { render() { return ( { this._node = node; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js new file mode 100644 index 00000000..ba100b4f --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -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 index = ({ language, item }) => { + const { target, client, language: lang } = language; + const snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client); + const { storedTheme } = useTheme(); + return ( + + ); +}; + +export default index; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js new file mode 100644 index 00000000..4b6eec55 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + margin-inline: -1rem; + margin-block: -1.5rem; + position: absolute; + 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; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js new file mode 100644 index 00000000..35f01972 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -0,0 +1,94 @@ +import Modal from 'components/Modal/index'; +import { useState } from 'react'; +import CodeView from './CodeView'; +import StyledWrapper from './StyledWrapper'; +import ErrorBoundary from 'src/pages/ErrorBoundary/index'; +import { isValidUrl } from 'utils/url/index'; + +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 index = ({ item, onClose }) => { + const [selectedLanguage, setSelectedLanguage] = useState(languages[0]); + return ( + + +
+
+
+ {languages && + languages.length && + languages.map((language) => ( +
setSelectedLanguage(language)} + > + {language.name} +
+ ))} +
+
+ {isValidUrl(item.request.url) ? ( + + ) : ( +
+
+

Invalid URL

+

Please check the URL and try again

+
+
+ )} +
+
+
+ ); +}; + +export default index; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3916cc2e..a59503be 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -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); @@ -166,6 +168,9 @@ const CollectionItem = ({ item, collection, searchText }) => { {runCollectionModalOpen && ( setRunCollectionModalOpen(false)} /> )} + {generateCodeItemModalOpen && ( + setGenerateCodeItemModalOpen(false)} /> + )}
drag(drop(node))}>
{indents && indents.length @@ -264,6 +269,18 @@ const CollectionItem = ({ item, collection, searchText }) => { Clone
)} + {!isFolder && item.type === 'http-request' && ( +
{ + e.stopPropagation(); + dropdownTippyRef.current.hide(); + setGenerateCodeItemModalOpen(true); + }} + > + Generate Code +
+ )}
{ diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js new file mode 100644 index 00000000..ada2854d --- /dev/null +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -0,0 +1,86 @@ +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 = (url) => { + const params = new URLSearchParams(url); + return params.forEach((value, name) => { + return { + name, + 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] + }; + } +}; + +const createUrl = (request) => { + let url = request.url; + const variablePattern = /\{\{([^}]+)\}\}/g; + const variables = request.url.match(variablePattern); + if (variables) { + variables.forEach((variable) => { + const variableName = variable.replaceAll('{', '').replaceAll('}', ''); + const variableValue = request.vars.req.find((v) => v.name === variableName).value; + url = url.replace(variable, variableValue); + }); + } + return url; +}; + +export const buildHarRequest = (request) => { + return { + method: request.method, + url: createUrl(request), + httpVersion: 'HTTP/1.1', + cookies: [], + headers: createHeaders(request.headers, request.body.mode), + queryString: createQuery(request.url), + postData: createPostData(request.body), + headersSize: 0, + bodySize: 0 + }; +}; diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index b28cc019..7f5a8e82 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -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; + } +};