From ce9cdc5293013903053b95ce28c87c6bdc47d519 Mon Sep 17 00:00:00 2001 From: Beedhan Date: Sun, 1 Oct 2023 23:55:39 +0545 Subject: [PATCH 01/16] fix: folder showing after deleting --- .../src/providers/ReduxStore/slices/collections/actions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index cfede2a40..4e0a7724e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -339,11 +339,10 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { if (!collection) { return reject(new Error('Collection not found')); } - const item = findItemInCollection(collection, itemUid); if (item) { const { ipcRenderer } = window; - + dispatch(_deleteItem({ itemUid, collectionUid })); ipcRenderer .invoke('renderer:delete-item', item.pathname, item.type) .then(() => resolve()) From cedcd2cf35fd50f8400185921f9a45a3c2a2cdfd Mon Sep 17 00:00:00 2001 From: Beedhan Date: Mon, 2 Oct 2023 00:00:03 +0545 Subject: [PATCH 02/16] Revert "fix: folder showing after deleting" This reverts commit ce9cdc5293013903053b95ce28c87c6bdc47d519. --- .../src/providers/ReduxStore/slices/collections/actions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 4e0a7724e..cfede2a40 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -339,10 +339,11 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { if (!collection) { return reject(new Error('Collection not found')); } + const item = findItemInCollection(collection, itemUid); if (item) { const { ipcRenderer } = window; - dispatch(_deleteItem({ itemUid, collectionUid })); + ipcRenderer .invoke('renderer:delete-item', item.pathname, item.type) .then(() => resolve()) From 978d810473f53e4bb63889657804bc60d98da1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szymborski?= Date: Sun, 1 Oct 2023 20:57:03 +0200 Subject: [PATCH 03/16] fix(#154): correct ordering during drag-and-drop When dragging and dropping items, to delay changing sequence numbers until after all the resource-dependent logic has completed, we were relying on the order of children in redux store (which we then converted into new seq numbers). This order of children was however not updated when sequence numbers changed (for example due to file watch changes). This resulted in a seemingly random drag-and-drop ordering, which in fact was linked to the initial order when the collection was loaded. This change sorts all the items by sequence number prior to reordering, so that those random jumps no longer happen. As this happens on a deep clone of the collection, no data gets hurt in the process. fixes #154 --- packages/bruno-app/src/utils/collections/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 80fe41dd3..0a20cb448 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -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); From cf6ec4e84fdd5cb3a51f69db972b233e7d48cdc1 Mon Sep 17 00:00:00 2001 From: not-known-person Date: Sun, 1 Oct 2023 15:22:05 -0400 Subject: [PATCH 04/16] fixed issue-#251-&-#265 --- .../providers/ReduxStore/slices/collections/actions.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index cfede2a40..642d7cd92 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -262,7 +262,10 @@ 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(() => { + dispatch(_renameItem({ newName, itemUid, collectionUid })) + resolve() + }).catch(reject); }); }; @@ -346,7 +349,10 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { ipcRenderer .invoke('renderer:delete-item', item.pathname, item.type) - .then(() => resolve()) + .then(() => { + dispatch(_deleteItem({ itemUid, collectionUid })) + resolve() + }) .catch((error) => reject(error)); } return; From fcc12fb089472716328054665d767d7b0ae48e04 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 04:17:35 +0530 Subject: [PATCH 05/16] fix(#251, #265): phantoms folders fix on rename/delete needs to be run only on windows --- .../ReduxStore/slices/collections/actions.js | 33 ++++++++++++++----- .../bruno-app/src/utils/common/platform.js | 8 +++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 9ceb90ed5..b91c7c8fe 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -22,7 +22,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 { @@ -263,10 +263,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta } const { ipcRenderer } = window; - ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(() => { - dispatch(_renameItem({ newName, itemUid, collectionUid })) - 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); }); }; @@ -351,8 +360,14 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { ipcRenderer .invoke('renderer:delete-item', item.pathname, item.type) .then(() => { - dispatch(_deleteItem({ itemUid, collectionUid })) - resolve() + // 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)); } @@ -361,8 +376,8 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { }; export const sortCollections = () => (dispatch) => { - dispatch(_sortCollections()) -} + dispatch(_sortCollections()); +}; export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index d144796e7..e49a66ec9 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -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'); +}; From 1804454ff021eda6f17e15a904abc42982a0d21c Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 05:26:47 +0530 Subject: [PATCH 06/16] chore: bump version v0.16.6 --- packages/bruno-app/src/components/Sidebar/index.js | 2 +- packages/bruno-electron/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 14d92d42f..cb0f0ffb5 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -116,7 +116,7 @@ const Sidebar = () => { )} -
v0.16.5
+
v0.16.6
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index ce46ee3c3..2807e2f97 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v0.16.5", + "version": "v0.16.6", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", From 3d0c9cc0ae045939a31eab4134b6eff3016ba271 Mon Sep 17 00:00:00 2001 From: Beedhan Date: Mon, 2 Oct 2023 13:58:25 +0545 Subject: [PATCH 07/16] feat:added code generator to http-requests --- packages/bruno-app/package.json | 1 + .../src/components/CodeEditor/index.js | 2 +- .../GenerateCodeItem/CodeView/index.js | 21 +++++ .../GenerateCodeItem/StyledWrapper.js | 39 ++++++++ .../CollectionItem/GenerateCodeItem/index.js | 94 +++++++++++++++++++ .../Collection/CollectionItem/index.js | 17 ++++ .../bruno-app/src/utils/codegenerator/har.js | 72 ++++++++++++++ packages/bruno-app/src/utils/url/index.js | 9 ++ 8 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js create mode 100644 packages/bruno-app/src/utils/codegenerator/har.js diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 4845116a5..7be069c17 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 bcc13b5c2..96d5bb48a 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 000000000..ba100b4fe --- /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 000000000..86772b5e1 --- /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; + + 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 000000000..35f01972f --- /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 3916cc2e6..a59503be0 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 000000000..241c1a64a --- /dev/null +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -0,0 +1,72 @@ +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] + }; + } +}; + +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.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 b28cc019e..7f5a8e825 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; + } +}; From ce0827308f621c9aa0c530db24aa8ba84f7976c1 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 13:47:14 +0530 Subject: [PATCH 08/16] chore: updated readme --- readme.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 6f902b14d..8d28bb6ec 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@
-### Bruno - Opensource IDE for exploring and testing APIs. +### 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,43 @@ [![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. +Bruno is offline-only. There are no plans to add cloud-sync to Bruno, Ever. We believe that your data is yours and should be stored on your machine. +If you are interested to know more about our long term vision, we recently penned some thoughts [here](https://github.com/usebruno/bruno/discussions/269) ![bruno](assets/images/landing-2.png)

### Run across multiple platforms 🖥️ + ![bruno](assets/images/run-anywhere.png)

### Collaborate via Git 👩‍💻🧑‍💻 + Or any version control system of your choice ![bruno](assets/images/version-control.png)

### 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 ❤️ +### Support ❤️ + Woof! If you like project, hit that ⭐ button !! ### Authors @@ -51,9 +58,11 @@ Woof! If you like project, hit that ⭐ button !!
### Stay in touch 🌐 + [Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq) ### License 📄 + [MIT](license.md) From b74922c8f3940b4a0146973c5536abf8d96ca825 Mon Sep 17 00:00:00 2001 From: Beedhan Date: Mon, 2 Oct 2023 14:02:54 +0545 Subject: [PATCH 09/16] bugfix:sidebar moving when code generator modal is opened --- .../Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 86772b5e1..4b6eec556 100644 --- 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 @@ -3,7 +3,7 @@ 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 { From 7297bb184ede2628bfdb9d890bf92e1d20502d54 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 13:58:09 +0530 Subject: [PATCH 10/16] chore: updated readme --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 8d28bb6ec..af60a1888 100644 --- a/readme.md +++ b/readme.md @@ -16,8 +16,7 @@ Bruno stores your collections directly in a folder on your filesystem. We use a 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 believe that your data is yours and should be stored on your machine. -If you are interested to know more about our long term vision, we recently penned some thoughts [here](https://github.com/usebruno/bruno/discussions/269) +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)

From 97200961eb68cd721932b0690c3f2018c98a052d Mon Sep 17 00:00:00 2001 From: Beedhan Date: Mon, 2 Oct 2023 14:24:13 +0545 Subject: [PATCH 11/16] feat: support for variables in url --- .../bruno-app/src/utils/codegenerator/har.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 241c1a64a..ada2854d7 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -57,10 +57,24 @@ const createPostData = (body) => { } }; +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: request.url, + url: createUrl(request), httpVersion: 'HTTP/1.1', cookies: [], headers: createHeaders(request.headers, request.body.mode), From bfc03f5ae4fc7dabd7fee8329fd7b74c104f1f9f Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 15:25:59 +0530 Subject: [PATCH 12/16] fix(#279): fixed codemirror crashes resulting in white screen --- .../ResponsePane/QueryResult/index.js | 53 +++++++++++++++---- .../src/components/ResponsePane/index.js | 5 +- .../RunnerResults/ResponsePane/index.js | 3 +- .../bruno-app/src/utils/common/codemirror.js | 22 ++++++++ packages/bruno-app/src/utils/common/index.js | 23 ++++---- 5 files changed, 82 insertions(+), 24 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index e6d6f94a8..4093dfcf9 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -3,10 +3,12 @@ import CodeEditor from 'components/CodeEditor'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common'; +import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror'; import StyledWrapper from './StyledWrapper'; -const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => { +const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => { const { storedTheme } = useTheme(); const dispatch = useDispatch(); @@ -17,17 +19,50 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener, dispatch(sendRequest(item, collection.uid)); }; + 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); + return (
- +
); diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index c8e071625..0b56840fa 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -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} /> ); } diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index ba87fcaf9..2c4f28b20 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -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} /> ); } diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index b1b60568c..aa4ba0519 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -42,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'; + } +}; diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 84725332f..c5eaa93ab 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => { } }; +export const safeParseXML = (str) => { + if (!str || !str.length || typeof str !== 'string') { + return str; + } + try { + return xmlFormat(str); + } catch (e) { + return str; + } +}; + // Remove any characters that are not alphanumeric, spaces, hyphens, or underscores export const normalizeFileName = (name) => { if (!name) { @@ -80,16 +91,6 @@ export const getContentType = (headers) => { 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; -}; From 636e14a3852f11df0c82c2b5a8bcc1f843e673b4 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 15:28:54 +0530 Subject: [PATCH 13/16] feat(#253): released cmd+h key back to native mac hide behaviour --- .../components/ResponsePane/Placeholder/index.js | 2 -- packages/bruno-app/src/providers/Hotkeys/index.js | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js index 0879ba520..2e7cd8621 100644 --- a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js @@ -13,13 +13,11 @@ const Placeholder = () => {
Send Request
New Request
Edit Environments
-
Help
Cmd + Enter
Cmd + B
Cmd + E
-
Cmd + H
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index a50e71dfb..522fa0d46 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -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 ( - {showBrunoSupportModal && setShowBrunoSupportModal(false)} />} {showSaveRequestModal && ( setShowSaveRequestModal(false)} /> )} From 336ad38cb9f4126de0ddfa5bfdc652876be2c485 Mon Sep 17 00:00:00 2001 From: not-known-person Date: Mon, 2 Oct 2023 08:34:39 -0400 Subject: [PATCH 14/16] Improved feat-/#220 --- .../components/Sidebar/Collections/index.js | 32 ++++++++++++++++--- .../ReduxStore/slices/collections/actions.js | 10 +++--- .../ReduxStore/slices/collections/index.js | 21 +++++++++--- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 57b14a0d7..4d427ee35 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { IconSearch, IconFolders, IconSortAZ } from '@tabler/icons'; +import { IconSearch, IconFolders, IconArrowsSort, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons'; import Collection from '../Collections/Collection'; import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; @@ -11,6 +11,23 @@ import { sortCollections } from 'providers/ReduxStore/slices/collections/actions 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 (
@@ -20,9 +37,16 @@ const CollectionsBadge = () => { Collections
- + { + collections.length >= 1 && + } +
); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index b91c7c8fe..856431e9b 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -34,12 +34,12 @@ import { renameItem as _renameItem, cloneItem as _cloneItem, deleteItem as _deleteItem, - sortCollections as _sortCollections, saveRequest as _saveRequest, selectEnvironment as _selectEnvironment, createCollection as _createCollection, renameCollection as _renameCollection, removeCollection as _removeCollection, + sortCollections as _sortCollections, collectionAddEnvFileEvent as _collectionAddEnvFileEvent } from './index'; @@ -145,7 +145,10 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) => }) .catch((err) => console.log(err)); }; - +export const sortCollections = (order) => (dispatch) => { + console.log("working") + dispatch(_sortCollections(order)) +} export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -375,9 +378,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { }); }; -export const sortCollections = () => (dispatch) => { - dispatch(_sortCollections()); -}; export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 8227efc6b..24ad23a8d 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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,13 +39,14 @@ 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; + console.log(collection) collapseCollection(collection); addDepth(collection.items); @@ -70,8 +72,19 @@ export const collectionsSlice = createSlice({ removeCollection: (state, action) => { state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid); }, - sortCollections: (state) => { - state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name)) + 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; From cbdd56e57783442c4a0ef2dbbceb65df30c9f146 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 15:38:50 +0530 Subject: [PATCH 15/16] chore: added some todo's for future code cleanup --- .../components/Sidebar/Collections/index.js | 55 +++++++++++-------- .../ReduxStore/slices/collections/actions.js | 8 ++- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 4d427ee35..af54350e4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,6 +1,12 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { IconSearch, IconFolders, IconArrowsSort, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons'; +import { + IconSearch, + IconFolders, + IconArrowsSort, + IconSortAscendingLetters, + IconSortDescendingLetters +} from '@tabler/icons'; import Collection from '../Collections/Collection'; import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; @@ -9,44 +15,47 @@ 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 dispatch = useDispatch(); const { collections } = useSelector((state) => state.collections); const { collectionSortOrder } = useSelector((state) => state.collections); const sortCollectionOrder = () => { let order; switch (collectionSortOrder) { case 'default': - order = 'alphabetical' + order = 'alphabetical'; break; case 'alphabetical': - order = 'reverseAlphabetical' + order = 'reverseAlphabetical'; break; case 'reverseAlphabetical': - order = 'default' + order = 'default'; break; } - dispatch(sortCollections({ order })) - } + dispatch(sortCollections({ order })); + }; return (
-
+
Collections
- { - collections.length >= 1 && - } - + )}
); @@ -95,12 +104,12 @@ const Collections = () => {
{collections && collections.length ? collections.map((c) => { - return ( - - - - ); - }) + return ( + + + + ); + }) : null}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 856431e9b..0c6945ae9 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -145,10 +145,12 @@ 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) => { - console.log("working") - dispatch(_sortCollections(order)) -} + dispatch(_sortCollections(order)); +}; export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); From 95532102baf9e5945f964db0b8b71f905af17a44 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 2 Oct 2023 16:52:02 +0530 Subject: [PATCH 16/16] feat(#280): code generator polishing and support url interpolation --- packages/bruno-app/package.json | 1 + .../GenerateCodeItem/CodeView/index.js | 28 +++--- .../GenerateCodeItem/StyledWrapper.js | 1 - .../CollectionItem/GenerateCodeItem/index.js | 85 +++++++++++++++---- .../Collection/CollectionItem/index.js | 2 +- .../ReduxStore/slices/collections/index.js | 11 ++- .../bruno-app/src/utils/codegenerator/har.js | 27 ++---- 7 files changed, 95 insertions(+), 60 deletions(-) diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 7be069c17..625e9dc0d 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", + "handlebars": "^4.7.8", "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", 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 index ba100b4fe..79d636daf 100644 --- 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 @@ -1,21 +1,21 @@ import CodeEditor from 'components/CodeEditor/index'; -import HTTPSnippet from 'httpsnippet'; +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 CodeView = ({ language, item }) => { const { storedTheme } = useTheme(); - return ( - - ); + 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 ; }; -export default index; +export default CodeView; 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 index 4b6eec556..f1c1c33e4 100644 --- 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 @@ -3,7 +3,6 @@ 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 { 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 index 35f01972f..509a529bd 100644 --- 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 @@ -2,8 +2,28 @@ 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'; +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 = [ { @@ -52,12 +72,32 @@ const languages = [ client: 'httpie' } ]; -const index = ({ item, onClose }) => { + +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 ( - - -
+ + +
{languages && @@ -75,20 +115,31 @@ const index = ({ item, onClose }) => { ))}
- {isValidUrl(item.request.url) ? ( - - ) : ( -
-
-

Invalid URL

-

Please check the URL and try again

+
+ {isValidUrl(interpolatedUrl) ? ( + + ) : ( +
+
+

Invalid URL: {interpolatedUrl}

+

Please check the URL and try again

+
-
- )} + )} +
- - + + ); }; -export default index; +export default GenerateCodeItem; 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 a59503be0..daed44279 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 @@ -169,7 +169,7 @@ const CollectionItem = ({ item, collection, searchText }) => { setRunCollectionModalOpen(false)} /> )} {generateCodeItemModalOpen && ( - setGenerateCodeItemModalOpen(false)} /> + setGenerateCodeItemModalOpen(false)} /> )}
drag(drop(node))}>
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 24ad23a8d..213761029 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -44,9 +44,8 @@ export const collectionsSlice = createSlice({ // 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.importedAt = new Date().getTime(); collection.lastAction = null; - console.log(collection) collapseCollection(collection); addDepth(collection.items); @@ -73,16 +72,16 @@ export const collectionsSlice = createSlice({ state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid); }, sortCollections: (state, action) => { - state.collectionSortOrder = action.payload.order + state.collectionSortOrder = action.payload.order; switch (action.payload.order) { case 'default': - state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt) + 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)) + 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)) + state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name)); break; } }, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index ada2854d7..b48fbc3c7 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -30,12 +30,11 @@ const createHeaders = (headers, mode) => { return headersArray; }; -const createQuery = (url) => { - const params = new URLSearchParams(url); - return params.forEach((value, name) => { +const createQuery = (queryParams = []) => { + return queryParams.map((param) => { return { - name, - value + name: param.name, + value: param.value }; }); }; @@ -57,28 +56,14 @@ const createPostData = (body) => { } }; -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), + url: request.url, httpVersion: 'HTTP/1.1', cookies: [], headers: createHeaders(request.headers, request.body.mode), - queryString: createQuery(request.url), + queryString: createQuery(request.params), postData: createPostData(request.body), headersSize: 0, bodySize: 0