- Enabled
+ Enabled
Name
Value
- Secret
+ Secret
@@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
-
- {variable.secret ? (
- {maskInputValue(variable.value)}
- ) : (
+
+
formik.setFieldValue(`${index}.value`, newValue, true)}
/>
- )}
+
-
+
{
Pre and post-request scripts that will run before and after any request inside this folder is sent.
-
-
Pre Request
+
+
Pre Request
{
font={get(preferences, 'font.codeFont', 'default')}
/>
-
-
Post Response
+
+
Post Response
{
};
return (
-
+
setTab('headers')}>
diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
index 65cb9c23b..f834fdaba 100644
--- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
+++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
@@ -2,7 +2,6 @@ import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div`
background: transparent;
- height: inherit;
.markdown-body {
background: transparent;
overflow-y: auto;
diff --git a/packages/bruno-app/src/components/Preferences/Support/index.js b/packages/bruno-app/src/components/Preferences/Support/index.js
index dfd6fabed..5e1b0dacc 100644
--- a/packages/bruno-app/src/components/Preferences/Support/index.js
+++ b/packages/bruno-app/src/components/Preferences/Support/index.js
@@ -1,39 +1,42 @@
import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
+import { useDictionary } from 'providers/Dictionary/index';
const Support = () => {
+ const { dictionary } = useDictionary();
+
return (
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js
index 41820a0c8..a44cecc1b 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js
@@ -150,6 +150,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
index bbe16ec70..8582a53cd 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
@@ -69,6 +69,7 @@ const BasicAuth = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
index 1dfa42b15..bef4d062a 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
@@ -43,6 +43,7 @@ const BearerAuth = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
index 24f4610f0..e91ed8d1f 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
@@ -69,6 +69,7 @@ const DigestAuth = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
index 793be57f0..2bb5dcc35 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
@@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -93,6 +93,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
index 67bc277aa..a100ce8e5 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
index df08475e8..a43c8f0ad 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
@@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -56,6 +56,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
index 164dcaab4..f2cd88ae3 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
index cfcff9784..4ec8c1faa 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
@@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -58,6 +58,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
index 6366bb5e7..32f2c999c 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'password',
- label: 'Password'
+ label: 'Password',
+ isSecret: true
},
{
key: 'clientId',
@@ -17,7 +18,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
index df90082c6..c7d66aadb 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
@@ -137,7 +137,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
) : null}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
index 06f9e4b78..3832f60c0 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
@@ -4,6 +4,7 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
+ flex: 1 1 0;
}
textarea.cm-editor {
diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
index 03b60d1d7..0c2707ac8 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
@@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/>
{
e.stopPropagation();
if (!item.draft) return;
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
index 83ebd8140..42da81d61 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
@@ -1,10 +1,6 @@
import styled from 'styled-components';
const Wrapper = styled.div`
- div.CodeMirror {
- /* todo: find a better way */
- height: calc(100vh - 220px);
- }
`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/RequestPane/Script/index.js b/packages/bruno-app/src/components/RequestPane/Script/index.js
index 935b52ede..acd674ee0 100644
--- a/packages/bruno-app/src/components/RequestPane/Script/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Script/index.js
@@ -40,8 +40,8 @@ const Script = ({ item, collection }) => {
return (
-
-
Pre Request
+
+
Pre Request
{
onSave={onSave}
/>
-
-
Post Response
+
+
Post Response
{
return (
-
+
-
+
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
index db0e45e41..cb62ac8a0 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
@@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
return (
-
+
Request no longer exists.
This can happen when the .bru file associated with this request was deleted on your filesystem.
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index 2fd253f4b..ecec4bc9d 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -158,10 +158,9 @@ const RequestTabPanel = () => {
{item.type === 'graphql-request' ? (
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
index 680782169..8b9bb0c35 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
@@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
+import Dropdown from 'components/Dropdown';
+import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
+import NewRequest from 'components/Sidebar/NewRequest/index';
-const RequestTab = ({ tab, collection, folderUid }) => {
+const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
+ const dropdownTippyRef = useRef();
+ const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
+
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
@@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
+ const handleRightClick = (_event) => {
+ const menuDropdown = dropdownTippyRef.current;
+ if (!menuDropdown) {
+ return;
+ }
+
+ if (menuDropdown.state.isShown) {
+ menuDropdown.hide();
+ } else {
+ menuDropdown.show();
+ }
+ };
+
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
@@ -143,6 +162,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
)}
{
if (!item.draft) return handleMouseUp(e);
@@ -159,6 +179,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
{item.name}
+
{
);
};
+function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
+ const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
+ const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
+
+ const totalTabs = collectionRequestTabs.length || 0;
+ const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
+ const currentTabItem = findItemInCollection(collection, currentTabUid);
+
+ const hasLeftTabs = tabIndex !== 0;
+ const hasRightTabs = totalTabs > tabIndex + 1;
+ const hasOtherTabs = totalTabs > 1;
+
+ async function handleCloseTab(event, tabUid) {
+ event.stopPropagation();
+ dropdownTippyRef.current.hide();
+
+ if (!tabUid) {
+ return;
+ }
+
+ try {
+ const item = findItemInCollection(collection, tabUid);
+ // silently save unsaved changes before closing the tab
+ if (item.draft) {
+ await dispatch(saveRequest(item.uid, collection.uid, true));
+ }
+
+ dispatch(closeTabs({ tabUids: [tabUid] }));
+ } catch (err) {}
+ }
+
+ function handleCloseOtherTabs(event) {
+ dropdownTippyRef.current.hide();
+
+ const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
+ otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseTabsToTheLeft(event) {
+ dropdownTippyRef.current.hide();
+
+ const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
+ leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseTabsToTheRight(event) {
+ dropdownTippyRef.current.hide();
+
+ const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
+ rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseSavedTabs(event) {
+ event.stopPropagation();
+
+ const savedTabs = collection.items.filter((item) => !item.draft);
+ const savedTabIds = savedTabs.map((item) => item.uid) || [];
+ dispatch(closeTabs({ tabUids: savedTabIds }));
+ }
+
+ function handleCloseAllTabs(event) {
+ collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ return (
+
+ {showAddNewRequestModal && (
+ setShowAddNewRequestModal(false)} />
+ )}
+
+ {showCloneRequestModal && (
+ setShowCloneRequestModal(false)}
+ />
+ )}
+
+ } placement="bottom-start">
+ {
+ dropdownTippyRef.current.hide();
+ setShowAddNewRequestModal(true);
+ }}
+ >
+ New Request
+
+ {
+ dropdownTippyRef.current.hide();
+ setShowCloneRequestModal(true);
+ }}
+ >
+ Clone Request
+
+ handleCloseTab(e, currentTabUid)}>
+ Close
+
+
+ Close Others
+
+
+ Close to the Left
+
+
+ Close to the Right
+
+
+ Close Saved
+
+
+ Close All
+
+
+
+ );
+}
+
export default RequestTab;
diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
index ec76ec5b5..93829cca9 100644
--- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
@@ -7,13 +7,14 @@ const Wrapper = styled.div`
padding: 0;
margin: 0;
display: flex;
- position: relative;
overflow: scroll;
&::-webkit-scrollbar {
display: none;
}
+ scrollbar-width: none;
+
li {
display: inline-flex;
max-width: 150px;
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index fbafb55cf..d0cd0b459 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -110,7 +110,14 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
-
+
);
})
diff --git a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
index 0178b90d7..38dd7511e 100644
--- a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
@@ -1,6 +1,19 @@
import styled from 'styled-components';
const Wrapper = styled.div`
+ .textbox {
+ border: 1px solid #ccc;
+ padding: 0.2rem 0.5rem;
+ box-shadow: none;
+ border-radius: 0px;
+ outline: none;
+ box-shadow: none;
+ transition: border-color ease-in-out 0.1s;
+ border-radius: 3px;
+ background-color: ${(props) => props.theme.modal.input.bg};
+ border: 1px solid ${(props) => props.theme.modal.input.border};
+ }
+
.item-path {
.link {
color: ${(props) => props.theme.textLink};
diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx
index e415aeb3c..4b0b68cba 100644
--- a/packages/bruno-app/src/components/RunnerResults/index.jsx
+++ b/packages/bruno-app/src/components/RunnerResults/index.jsx
@@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null);
+ const [delay, setDelay] = useState(null);
// ref for the runner output body
const runnerBodyRef = useRef();
@@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
.filter(Boolean);
const runCollection = () => {
- dispatch(runCollectionFolder(collection.uid, null, true));
+ dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
};
const runAgain = () => {
- dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
+ dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
};
const resetRunner = () => {
@@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
You have {totalRequestsInCollection} requests in this collection.
+
+ Delay (in ms)
+ setDelay(e.target.value)}
+ />
+
+
Run Collection
@@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
{item.status !== 'error' && item.status !== 'completed' ? (
- ) : (
+ ) : item.responseReceived?.status ? (
setSelectedItem(item)}>
- ({get(item.responseReceived, 'status')}
- {get(item.responseReceived, 'statusText')} )
+ ({item.responseReceived?.status}
+ {item.responseReceived?.statusText} )
+
+ ) : (
+
setSelectedItem(item)}>
+ (request failed)
)}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
index 55c2b86dd..0dd96e197 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
@@ -58,6 +58,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
id="collection-item-name"
type="text"
name="name"
+ placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
index 418658f03..ff06f4f31 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
@@ -2,6 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
+ height: 100%;
.copy-to-clipboard {
position: absolute;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js
index e5a657ef9..6553be58f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js
@@ -91,13 +91,13 @@ const Collections = () => {
setSearchText(e.target.value.toLowerCase())}
/>
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
index 4211e8ff1..7dd827298 100644
--- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
+++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
@@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
collectionLocation: Yup.string()
.min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less')
- .required('name is required')
+ .required('Location is required')
}),
onSubmit: (values) => {
handleSubmit(values.collectionLocation);
@@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
- formik.setFieldValue('collectionLocation', dirPath);
+ if (typeof dirPath === 'string' && dirPath.length > 0) {
+ formik.setFieldValue('collectionLocation', dirPath);
+ }
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -160,7 +162,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
type="text"
name="collectionLocation"
readOnly={true}
- className="block textbox mt-2 w-full"
+ className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index 8d8125e94..50e7be277 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
- body: request.body
+ body: request.body,
+ auth: request.auth
})
)
.then(() => onClose())
@@ -161,7 +162,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
return (
-
diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js
index dbb46191b..31d0875fd 100644
--- a/packages/bruno-app/src/components/SingleLineEditor/index.js
+++ b/packages/bruno-app/src/components/SingleLineEditor/index.js
@@ -1,8 +1,9 @@
import React, { Component } from 'react';
import isEqual from 'lodash/isEqual';
import { getAllVariables } from 'utils/collections';
-import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
+import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
+import { IconEye, IconEyeOff } from '@tabler/icons';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
this.cachedValue = props.value || '';
this.editorRef = React.createRef();
this.variables = {};
+
+ this.state = {
+ maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
+ };
}
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
+ const runHandler = () => {
+ if (this.props.onRun) {
+ this.props.onRun();
+ }
+ };
+ const saveHandler = () => {
+ if (this.props.onSave) {
+ this.props.onSave();
+ }
+ };
+ const noopHandler = () => {};
+
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
@@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
scrollbarStyle: null,
tabindex: 0,
extraKeys: {
- Enter: () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Ctrl-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Cmd-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
+ Enter: runHandler,
+ 'Ctrl-Enter': runHandler,
+ 'Cmd-Enter': runHandler,
'Alt-Enter': () => {
if (this.props.allowNewlines) {
this.editor.setValue(this.editor.getValue() + '\n');
@@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
this.props.onRun();
}
},
- 'Shift-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Cmd-S': () => {
- if (this.props.onSave) {
- this.props.onSave();
- }
- },
- 'Ctrl-S': () => {
- if (this.props.onSave) {
- this.props.onSave();
- }
- },
- 'Cmd-F': () => {},
- 'Ctrl-F': () => {},
+ 'Shift-Enter': runHandler,
+ 'Cmd-S': saveHandler,
+ 'Ctrl-S': saveHandler,
+ 'Cmd-F': noopHandler,
+ 'Ctrl-F': noopHandler,
// Tabbing disabled to make tabindex work
Tab: false,
'Shift-Tab': false
@@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay(variables);
+ this._enableMaskedEditor(this.props.isSecret);
+ this.setState({ maskInput: this.props.isSecret });
}
+ /** Enable or disable masking the rendered content of the editor */
+ _enableMaskedEditor = (enabled) => {
+ if (typeof enabled !== 'boolean') return;
+
+ console.log('Enabling masked editor: ' + enabled);
+ if (enabled == true) {
+ if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
+ this.maskedEditor.enable();
+ } else {
+ this.maskedEditor?.disable();
+ this.maskedEditor = null;
+ }
+ };
+
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();
@@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
this.cachedValue = String(this.props.value);
this.editor.setValue(String(this.props.value) || '');
}
+ if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
+ // If the secret flag has changed, update the editor to reflect the change
+ this._enableMaskedEditor(this.props.isSecret);
+ // also set the maskInput flag to the new value
+ this.setState({ maskInput: this.props.isSecret });
+ }
this.ignoreChangeEvent = false;
}
@@ -135,8 +150,35 @@ class SingleLineEditor extends Component {
this.editor.setOption('mode', 'brunovariables');
};
+ toggleVisibleSecret = () => {
+ const isVisible = !this.state.maskInput;
+ this.setState({ maskInput: isVisible });
+ this._enableMaskedEditor(isVisible);
+ };
+
+ /**
+ * @brief Eye icon to show/hide the secret value
+ * @returns ReactComponent The eye icon
+ */
+ secretEye = (isSecret) => {
+ return isSecret === true ? (
+
this.toggleVisibleSecret()}>
+ {this.state.maskInput === true ? (
+
+ ) : (
+
+ )}
+
+ ) : null;
+ };
+
render() {
- return
;
+ return (
+
+
+ {this.secretEye(this.props.isSecret)}
+
+ );
}
}
export default SingleLineEditor;
diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js
index 385a71486..54f7b5378 100644
--- a/packages/bruno-app/src/components/Welcome/index.js
+++ b/packages/bruno-app/src/components/Welcome/index.js
@@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import StyledWrapper from './StyledWrapper';
+import { useDictionary } from 'providers/Dictionary/index';
const Welcome = () => {
const dispatch = useDispatch();
+ const { dictionary } = useDictionary();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@@ -20,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
- (err) => console.log(err) && toast.error('An error occurred while opening the collection')
+ (err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
);
};
@@ -38,12 +40,12 @@ const Welcome = () => {
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
- toast.success('Collection imported successfully');
+ toast.success(dictionary.collectionImportedSuccessfully);
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
- toast.error('An error occurred while importing the collection. Check the logs for more information.');
+ toast.error(dictionary.errorWhileImportingCollection);
});
};
@@ -66,46 +68,45 @@ const Welcome = () => {
bruno
-
Opensource IDE for exploring and testing APIs
+
{dictionary.aboutBruno}
-
Collections
+
{dictionary.collections}
setCreateCollectionModalOpen(true)}>
- Create Collection
+ {dictionary.createCollection}
- Open Collection
+ {dictionary.openCollection}
setImportCollectionModalOpen(true)}>
- Import Collection
+ {dictionary.importCollection}
-
Links
diff --git a/packages/bruno-app/src/dictionaries/en.js b/packages/bruno-app/src/dictionaries/en.js
new file mode 100644
index 000000000..a9ff316cd
--- /dev/null
+++ b/packages/bruno-app/src/dictionaries/en.js
@@ -0,0 +1,16 @@
+export default {
+ aboutBruno: 'Opensource IDE for exploring and testing APIs',
+ collections: 'Collections',
+ createCollection: 'Create Collection',
+ openCollection: 'Open Collection',
+ importCollection: 'Import Collection',
+ documentation: 'Documentation',
+ reportIssues: 'Report Issues',
+ gitHub: 'GitHub',
+ collectionImportedSuccessfully: 'Collection imported successfully',
+ errorWhileOpeningCollection: 'An error occurred while opening the collection',
+ errorWhileImportingCollection:
+ 'An error occurred while importing the collection. Check the logs for more information.',
+ discord: 'Discord',
+ twitter: 'Twitter'
+};
diff --git a/packages/bruno-app/src/dictionaries/index.js b/packages/bruno-app/src/dictionaries/index.js
new file mode 100644
index 000000000..fb5f797dc
--- /dev/null
+++ b/packages/bruno-app/src/dictionaries/index.js
@@ -0,0 +1,5 @@
+import en from './en.js';
+
+export const dictionaries = {
+ en
+};
diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js
index 25a6d15bc..7839a55ac 100644
--- a/packages/bruno-app/src/globalStyles.js
+++ b/packages/bruno-app/src/globalStyles.js
@@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
}
}
+ input::placeholder {
+ color: ${(props) => props.theme.input.placeholder.color};
+ opacity: ${(props) => props.theme.input.placeholder.opacity};
+ }
+
@keyframes fade-in {
from {
opacity: 0;
diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js
index cf8b3683e..d2bf8a28d 100644
--- a/packages/bruno-app/src/pages/_app.js
+++ b/packages/bruno-app/src/pages/_app.js
@@ -14,6 +14,7 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
+import { DictionaryProvider } from 'providers/Dictionary/index';
function SafeHydrate({ children }) {
return
{typeof window === 'undefined' ? null : children}
;
@@ -59,13 +60,15 @@ function MyApp({ Component, pageProps }) {
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js
index c54d53867..7664ae03e 100644
--- a/packages/bruno-app/src/providers/App/index.js
+++ b/packages/bruno-app/src/providers/App/index.js
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+import { get } from 'lodash';
import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
import ConfirmAppClose from './ConfirmAppClose';
@@ -18,6 +19,13 @@ export const AppProvider = (props) => {
dispatch(refreshScreenWidth());
}, []);
+ useEffect(() => {
+ const platform = get(navigator, 'platform', '');
+ if(platform && platform.toLowerCase().indexOf('mac') > -1) {
+ document.body.classList.add('os-mac');
+ }
+ }, []);
+
useEffect(() => {
const handleResize = () => {
dispatch(refreshScreenWidth());
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index ae379e91e..bd5551c91 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.22.0'
+ version: '1.25.0'
}
});
};
diff --git a/packages/bruno-app/src/providers/Dictionary/index.js b/packages/bruno-app/src/providers/Dictionary/index.js
new file mode 100644
index 000000000..75a399f27
--- /dev/null
+++ b/packages/bruno-app/src/providers/Dictionary/index.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { useState, useContext } from 'react';
+import { dictionaries } from 'src/dictionaries/index';
+
+export const DictionaryContext = React.createContext();
+
+const DictionaryProvider = (props) => {
+ const [language, setLanguage] = useState('en');
+ const dictionary = dictionaries[language] ?? dictionaries.en;
+
+ return (
+
+ <>{props.children}>
+
+ );
+};
+
+const useDictionary = () => {
+ const context = useContext(DictionaryContext);
+
+ if (context === undefined) {
+ throw new Error(`useDictionary must be used within a DictionaryProvider`);
+ }
+
+ return context;
+};
+
+export { useDictionary, DictionaryProvider };
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index 8b0503b1c..53a0fc263 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
+ // close all tabs
+ useEffect(() => {
+ Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
+ const activeTab = find(tabs, (t) => t.uid === activeTabUid);
+ if (activeTab) {
+ const collection = findCollectionByUid(collections, activeTab.collectionUid);
+
+ if (collection) {
+ const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
+ dispatch(
+ closeTabs({
+ tabUids: tabUids
+ })
+ );
+ }
+ }
+
+ return false; // this stops the event bubbling
+ });
+
+ return () => {
+ Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
+ };
+ }, [activeTabUid, tabs, collections, dispatch]);
+
return (
{showSaveRequestModal && (
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 5e8984823..b434f6a96 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -39,7 +39,7 @@ import {
import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
-import { parseQueryParams, splitOnFirst } from 'utils/url/index';
+import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
@@ -192,10 +192,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
- const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
- const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
-
- _sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables, itemUid, secretVariables)
+ _sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@@ -284,7 +281,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
};
-export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
+export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -315,7 +312,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
collectionCopy,
environment,
collectionCopy.runtimeVariables,
- recursive
+ recursive,
+ delay
)
.then(resolve)
.catch((err) => {
@@ -700,7 +698,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
- const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
+ const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => {
const state = getState();
@@ -710,11 +708,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
const parts = splitOnFirst(requestUrl, '?');
- const params = parseQueryParams(parts[1]);
- each(params, (urlParam) => {
+ const queryParams = parseQueryParams(parts[1]);
+ each(queryParams, (urlParam) => {
urlParam.enabled = true;
+ urlParam.type = 'query';
});
+ const pathParams = parsePathParams(requestUrl);
+ each(pathParams, (pathParm) => {
+ pathParams.enabled = true;
+ pathParm.type = 'path'
+ });
+
+ const params = [...queryParams, ...pathParams];
+
const item = {
uid: uuid(),
type: requestType,
@@ -733,6 +740,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
multipartForm: null,
formUrlEncoded: null,
rawFile: null
+ },
+ auth: auth ?? {
+ mode: 'none'
}
}
};
diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css
index 29e9196ea..8396c48b5 100644
--- a/packages/bruno-app/src/styles/globals.css
+++ b/packages/bruno-app/src/styles/globals.css
@@ -58,6 +58,15 @@ body::-webkit-scrollbar-thumb,
border-radius: 5rem;
}
+/*
+ * Mac-specific scrollbar styling
+ * This ensures that scrollbars are only visible when the user starts to scroll,
+ * providing a cleaner and more minimalistic appearance.
+ */
+body.os-mac * {
+ scrollbar-width: thin;
+}
+
/*
* todo: this will be supported in the future to be changed via applying a theme
* making all the checkboxes and radios bigger
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index bb1001f31..9e8e923aa 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -20,7 +20,11 @@ const darkTheme = {
input: {
bg: 'rgb(65, 65, 65)',
border: 'rgb(65, 65, 65)',
- focusBorder: 'rgb(65, 65, 65)'
+ focusBorder: 'rgb(65, 65, 65)',
+ placeholder: {
+ color: '#a2a2a2',
+ opacity: 0.75
+ }
},
variables: {
@@ -154,7 +158,7 @@ const darkTheme = {
modal: {
title: {
color: '#ccc',
- bg: 'rgb(48, 48, 49)',
+ bg: 'rgb(38, 38, 39)',
iconColor: '#ccc'
},
body: {
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index a130f2513..a25583136 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -20,7 +20,11 @@ const lightTheme = {
input: {
bg: 'white',
border: '#ccc',
- focusBorder: '#8b8b8b'
+ focusBorder: '#8b8b8b',
+ placeholder: {
+ color: '#a2a2a2',
+ opacity: 0.8
+ }
},
menubar: {
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index 0e3476256..c17eef866 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -2,10 +2,16 @@ const createContentType = (mode) => {
switch (mode) {
case 'json':
return 'application/json';
+ case 'text':
+ return 'text/plain';
case 'xml':
return 'application/xml';
+ case 'sparql':
+ return 'application/sparql-query';
case 'formUrlEncoded':
return 'application/x-www-form-urlencoded';
+ case 'graphql':
+ return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
default:
@@ -13,13 +19,19 @@ const createContentType = (mode) => {
}
};
-const createHeaders = (headers) => {
- return headers
+const createHeaders = (request, headers) => {
+ const enabledHeaders = headers
.filter((header) => header.enabled)
.map((header) => ({
name: header.name,
value: header.value
}));
+
+ const contentType = createContentType(request.body?.mode);
+ if (contentType !== '') {
+ enabledHeaders.push({ name: 'content-type', value: contentType });
+ }
+ return enabledHeaders;
};
const createQuery = (queryParams = []) => {
@@ -54,7 +66,7 @@ export const buildHarRequest = ({ request, headers }) => {
url: encodeURI(request.url),
httpVersion: 'HTTP/1.1',
cookies: [],
- headers: createHeaders(headers),
+ headers: createHeaders(request, headers),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,
diff --git a/packages/bruno-app/src/utils/collections/search.js b/packages/bruno-app/src/utils/collections/search.js
index b420687b7..9c2f187e5 100644
--- a/packages/bruno-app/src/utils/collections/search.js
+++ b/packages/bruno-app/src/utils/collections/search.js
@@ -3,7 +3,7 @@ import filter from 'lodash/filter';
import find from 'lodash/find';
export const doesRequestMatchSearchText = (request, searchText = '') => {
- return request.name.toLowerCase().includes(searchText.toLowerCase());
+ return request?.name?.toLowerCase().includes(searchText.toLowerCase());
};
export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {
diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js
index f4013a366..cbb1a2b3a 100644
--- a/packages/bruno-app/src/utils/common/codemirror.js
+++ b/packages/bruno-app/src/utils/common/codemirror.js
@@ -12,6 +12,64 @@ const pathFoundInVariables = (path, obj) => {
return value !== undefined;
};
+/**
+ * Changes the render behaviour for a given CodeMirror editor.
+ * Replaces all **rendered** characters, not the actual value, with the provided character.
+ */
+export class MaskedEditor {
+ /**
+ * @param {import('codemirror').Editor} editor CodeMirror editor instance
+ * @param {string} maskChar Target character being applied to all content
+ */
+ constructor(editor, maskChar) {
+ this.editor = editor;
+ this.maskChar = maskChar;
+ this.enabled = false;
+ }
+
+ /**
+ * Set and apply new masking character
+ */
+ enable = () => {
+ this.enabled = true;
+ this.editor.setValue(this.editor.getValue());
+ this.editor.on('inputRead', this.maskContent);
+ this.update();
+ };
+
+ /** Disables masking of the editor field. */
+ disable = () => {
+ this.enabled = false;
+ this.editor.off('inputRead', this.maskContent);
+ this.editor.setValue(this.editor.getValue());
+ };
+
+ /** Updates the rendered content if enabled. */
+ update = () => {
+ if (this.enabled) this.maskContent();
+ };
+
+ /** Replaces all rendered characters, with the provided character. */
+ maskContent = () => {
+ const content = this.editor.getValue();
+ this.editor.operation(() => {
+ // Clear previous masked text
+ this.editor.getAllMarks().forEach((mark) => mark.clear());
+ // Apply new masked text
+ for (let i = 0; i < content.length; i++) {
+ if (content[i] !== '\n') {
+ const maskedNode = document.createTextNode(this.maskChar);
+ this.editor.markText(
+ { line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
+ { line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
+ { replacedWith: maskedNode, handleMouseEvents: true }
+ );
+ }
+ }
+ });
+ };
+}
+
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
const { pathParams = {}, ...variables } = _variables || {};
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js
index 82eb0be95..e76f4014a 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.js
@@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => {
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
}
- requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
+ requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
@@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
- const splitAuth = request.auth.split(':');
- const user = splitAuth[0] || '';
- const password = splitAuth[1] || '';
-
- requestJson.auth = {
- user: repr(user),
- password: repr(password)
- };
+ if(request.auth.mode === 'basic'){
+ requestJson.auth = {
+ mode: 'basic',
+ basic: {
+ username: repr(request.auth.basic?.username),
+ password: repr(request.auth.basic?.password)
+ }
+ }
+ }
}
return Object.keys(requestJson).length ? requestJson : {};
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
index 2704bd4c5..2d9785154 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
@@ -75,4 +75,15 @@ describe('curlToJson', () => {
}
});
});
+
+ it('should return and parse a simple curl command with a trailing slash', () => {
+ const curlCommand = 'curl https://www.usebruno.com/';
+ const result = curlToJson(curlCommand);
+
+ expect(result).toEqual({
+ url: 'https://www.usebruno.com/',
+ raw_url: 'https://www.usebruno.com/',
+ method: 'get'
+ });
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js
index 97bfbd966..e16dc68a5 100644
--- a/packages/bruno-app/src/utils/curl/index.js
+++ b/packages/bruno-app/src/utils/curl/index.js
@@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
url: request.url,
method: request.method,
body,
- headers: headers
+ headers: headers,
+ auth: request.auth
};
} catch (error) {
console.error(error);
diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js
index 810d1af8a..79db23672 100644
--- a/packages/bruno-app/src/utils/curl/parse-curl.js
+++ b/packages/bruno-app/src/utils/curl/parse-curl.js
@@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => {
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
alias: {
H: 'header',
- A: 'user-agent'
+ A: 'user-agent',
+ u: 'user'
}
});
@@ -72,11 +73,10 @@ const parseCurlCommand = (curlCommand) => {
parsedArguments.header.forEach((header) => {
if (header.indexOf('Cookie') !== -1) {
cookieString = header;
- } else {
- const components = header.split(/:(.*)/);
- if (components[1]) {
- headers[components[0]] = components[1].trim();
- }
+ }
+ const components = header.split(/:(.*)/);
+ if (components[1]) {
+ headers[components[0]] = components[1].trim();
}
});
}
@@ -188,10 +188,21 @@ const parseCurlCommand = (curlCommand) => {
}
urlObject.search = null; // Clean out the search/query portion.
+
+ let urlWithoutQuery = URL.format(urlObject);
+ let urlHost = urlObject?.host;
+ if (!url?.includes(`${urlHost}/`)) {
+ if (urlWithoutQuery && urlHost) {
+ const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost);
+ urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1);
+ }
+ }
+
const request = {
- url: url,
- urlWithoutQuery: URL.format(urlObject)
+ url,
+ urlWithoutQuery
};
+
if (compressed) {
request.compressed = true;
}
@@ -227,12 +238,19 @@ const parseCurlCommand = (curlCommand) => {
request.data = parsedArguments['data-urlencode'];
}
- if (parsedArguments.u) {
- request.auth = parsedArguments.u;
- }
- if (parsedArguments.user) {
- request.auth = parsedArguments.user;
+ if (parsedArguments.user && typeof parsedArguments.user === 'string') {
+ const basicAuth = parsedArguments.user.split(':')
+ const username = basicAuth[0] || ''
+ const password = basicAuth[1] || ''
+ request.auth = {
+ mode: 'basic',
+ basic: {
+ username,
+ password
+ }
+ }
}
+
if (Array.isArray(request.data)) {
request.dataArray = request.data;
request.data = request.data.join('&');
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index f1a992e47..6c70fe66e 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v1.22.0",
+ "version": "v1.25.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 2fbd4cc98..589cd29d8 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -428,17 +428,16 @@ class Watcher {
this.watchers = {};
}
- addWatcher(win, watchPath, collectionUid, brunoConfig) {
+ addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
const ignores = brunoConfig?.ignore || [];
- const self = this;
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
- usePolling: watchPath.startsWith('\\\\') ? true : false,
+ usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
ignored: (filepath) => {
const normalizedPath = filepath.replace(/\\/g, '/');
const relativePath = path.relative(watchPath, normalizedPath);
@@ -457,14 +456,36 @@ class Watcher {
depth: 20
});
+ let startedNewWatcher = false;
watcher
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
- .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath));
+ .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
+ .on('error', (error) => {
+ // `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627
+ // `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached.
+ // To prevent loops `!forcePolling` is checked.
+ if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {
+ // This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and
+ // Multiple watcher being started `startedNewWatcher` is set to prevent this.
+ startedNewWatcher = true;
+ watcher.close();
+ console.error(
+ `\nCould not start watcher for ${watchPath}:`,
+ 'ENOSPC: System limit for number of file watchers reached!',
+ 'Trying again with polling, this will be slower!\n',
+ 'Update you system config to allow more concurrently watched files with:',
+ '"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
+ );
+ this.addWatcher(win, watchPath, collectionUid, brunoConfig, true);
+ } else {
+ console.error(`An error occurred in the watcher for: ${watchPath}`, error);
+ }
+ });
- self.watchers[watchPath] = watcher;
+ this.watchers[watchPath] = watcher;
}, 100);
}
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 7f4e58422..cad10a10c 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -50,6 +50,7 @@ app.on('ready', async () => {
height,
minWidth: 1000,
minHeight: 640,
+ show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
@@ -67,6 +68,9 @@ app.on('ready', async () => {
mainWindow.maximize();
}
+ mainWindow.once('ready-to-show', () => {
+ mainWindow.show();
+ })
const url = isDev
? 'http://localhost:3000'
: format({
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index c384ba71e..3aa819c4c 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -408,7 +408,7 @@ const registerNetworkIpc = (mainWindow) => {
}
// run post-response script
- const responseScript = compact(scriptingConfig.flow === 'natural' ? [
+ const responseScript = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.script.res'), get(request, 'script.res')
] : [
get(request, 'script.res'), get(collectionRoot, 'request.script.res')
@@ -596,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- const testFile = compact(scriptingConfig.flow === 'natural' ? [
+ const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript,
] : [
testScript, get(collectionRoot, 'request.tests')
@@ -825,7 +825,7 @@ const registerNetworkIpc = (mainWindow) => {
ipcMain.handle(
'renderer:run-collection-folder',
- async (event, folder, collection, environment, runtimeVariables, recursive) => {
+ async (event, folder, collection, environment, runtimeVariables, recursive, delay) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const folderUid = folder ? folder.uid : null;
@@ -944,6 +944,18 @@ const registerNetworkIpc = (mainWindow) => {
timeStart = Date.now();
let response, responseTime;
try {
+ if (delay && !Number.isNaN(delay) && delay > 0) {
+ const delayPromise = new Promise((resolve) => setTimeout(resolve, delay));
+
+ const cancellationPromise = new Promise((_, reject) => {
+ abortController.signal.addEventListener('abort', () => {
+ reject(new Error('Cancelled'));
+ });
+ });
+
+ await Promise.race([delayPromise, cancellationPromise]);
+ }
+
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
timeEnd = Date.now();
@@ -1036,7 +1048,7 @@ const registerNetworkIpc = (mainWindow) => {
// run tests
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- const testFile = compact(scriptingConfig.flow === 'natural' ? [
+ const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript
] : [
testScript, get(collectionRoot, 'request.tests')
@@ -1153,12 +1165,22 @@ const registerNetworkIpc = (mainWindow) => {
return `response.${extension}`;
};
- const fileName =
- getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader();
+ const getEncodingFormat = () => {
+ const contentType = getHeaderValue('content-type');
+ const extension = mime.extension(contentType) || 'txt';
+ return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64';
+ };
+ const determineFileName = () => {
+ return (
+ getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader()
+ );
+ };
+
+ const fileName = determineFileName();
const filePath = await chooseFileToSave(mainWindow, fileName);
if (filePath) {
- await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64'));
+ await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat()));
}
} catch (error) {
return Promise.reject(error);
diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
index 216c3be97..7a1a5b503 100644
--- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js
+++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
@@ -32,9 +32,6 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
client_secret: clientSecret,
state: state
};
- if (scope) {
- data['scope'] = scope;
- }
if (pkce) {
data['code_verifier'] = codeVerifier;
}
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 6e1af447b..28e13e002 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -18,8 +18,8 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
folderHeaders.set(header.name, header.value);
}
});
- } else {
- let headers = get(i, 'request.headers', []);
+ } else if (i.uid === request.uid) {
+ const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
headers.forEach((header) => {
if (header.enabled) {
folderHeaders.set(header.name, header.value);
@@ -55,8 +55,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => {
folderReqVars.set(_var.name, _var.value);
}
});
- } else {
- let vars = get(i, 'request.vars.req', []);
+ } else if (i.uid === request.uid) {
+ const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderReqVars.set(_var.name, _var.value);
@@ -91,8 +91,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => {
folderResVars.set(_var.name, _var.value);
}
});
- } else {
- let vars = get(i, 'request.vars.res', []);
+ } else if (i.uid === request.uid) {
+ const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderResVars.set(_var.name, _var.value);
@@ -147,7 +147,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
}
if (folderCombinedPostResScript.length) {
- if (scriptFlow === 'natural') {
+ if (scriptFlow === 'sequential') {
request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
@@ -155,7 +155,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
}
if (folderCombinedTests.length) {
- if (scriptFlow === 'natural') {
+ if (scriptFlow === 'sequential') {
request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
@@ -309,7 +309,7 @@ const prepareRequest = (item, collection) => {
}
});
- // scriptFlow is either "sandwich" or "natural"
+ // scriptFlow is either "sandwich" or "sequential"
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) {
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 29f469922..dc410945b 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -13,7 +13,7 @@ const stripLastLine = (text) => {
};
const getValueString = (value) => {
- const hasNewLines = value.includes('\n');
+ const hasNewLines = value?.includes('\n');
if (!hasNewLines) {
return value;
@@ -269,7 +269,6 @@ ${indentString(body.rawFile)}
multipartForms
.map((item) => {
const enabled = item.enabled ? '' : '~';
-
if (item.type === 'text') {
return `${enabled}${item.name}: ${getValueString(item.value)}`;
}
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 35ec2979f..97b4f4ba4 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -48,7 +48,7 @@ const varsSchema = Yup.object({
const requestUrlSchema = Yup.string().min(0).defined();
const requestMethodSchema = Yup.string()
- .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])
+ .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'])
.required('method is required');
const graphqlBodySchema = Yup.object({
diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js
index 87399c690..9fd223cb2 100644
--- a/packages/bruno-schema/src/collections/requestSchema.spec.js
+++ b/packages/bruno-schema/src/collections/requestSchema.spec.js
@@ -32,7 +32,7 @@ describe('Request Schema Validation', () => {
return Promise.all([
expect(requestSchema.validate(request)).rejects.toEqual(
validationErrorWithMessages(
- 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
+ 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE'
)
)
]);