Compare commits

..

17 Commits

Author SHA1 Message Date
Anoop M D
adb04fb0ff feat: update proxy implementation in preferences 2024-08-30 12:43:23 +05:30
lohit
c1ec95dc29 fix: option to toggle on/off system proxy env variables (#2724)
fix: option to toggle on/off system proxy env variables
2024-08-30 11:44:29 +05:30
Mateusz Pietryga
93080de2a8 fix: Issue with Parameters Passed in the URL(#2124) (#2139)
* fix: Issue with Parameters Passed in the URL(#2124)

The '=' should be allowed within query parameter value. While first equals sign separates parameter name from its value, any subsequent occurrences are valid and should not be discarded.
The '#' in URL always indicates the start of URI Fragment component, and should not be treated as part of any parameter value.

* chore: gracefully fail when URLSearchParams throws error

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-29 12:46:20 +05:30
dev.paramjot
36ef38be6a feat(#736): Switch tabs with keyboard shortcut (#812)
* feat(#736): Switch tabs with keyboard shortcut

1. Registered keyboard events in Hotkeys/index.js
2. Added logic for replacing `state.activeTabUid` to switch active tab as per keyboard event.
3. Maintained a stack `recentUsedTabsStack` for tab visit history and pop out on `Ctrl+Tab` keyboard event.

* feat(#736): Switch tabs with keyboard shortcut

Keeping this feature request only limited to CTRL+PGUP and CTRL_PGDN button clicks functionality. Hence removing logic for CTRL+TAB click functionality.

* feat(#736): Switch tabs with keyboard shortcut

clean up

* feate(#827): Switch tabs with keyboard shortcut

* Implimented logic of cyclic traversal of tabs array with % opreator.

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-29 11:12:28 +05:30
Pragadesh-45
4726f5008e style chore: make delete div bg important (#2949) 2024-08-29 10:43:32 +05:30
Anoop M D
091b02c2c3 release: v1.27.0 2024-08-28 15:10:28 +05:30
Pragadesh-45
600940226c Feat/icon tooltip (#2812)
* comp-rename Tooltip to InfoTip (fbu)

* fix: additional func InfoTip

* ToolHint component

* toolhint intg collectiontoolbar

* toolhint intg notifications

* toolhint intg sidebar

* chore: update infotip for path params

* chore: update infotip for path params
2024-08-27 18:36:59 +05:30
Max Bauer
ee7f886c03 feat: adjust code editor font size (#2204)
* add font-size setting for code editor

* add code font size to remaining editors

* align font-size after font-family

* changed default font size to 14

* fixed className typo

* set inherit mode if unset

* add code font size schema validation

* add font size to folder settings

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-27 18:26:58 +05:30
Daniel Kocot
682c7bd1b1 Aligned the correct form of address to make it easier to read (#2176)
* Aligned the correct form of address to make it easier to read

* typo

Co-authored-by: Andreas Siegel <mail@andreassiegel.de>

---------

Co-authored-by: Andreas Siegel <mail@andreassiegel.de>
2024-08-27 17:33:06 +05:30
KameronKeller
e1aa5b4eb5 Feat (#2284): Feature: Add Table of Contents to the Readme (#2285)
* Add table of contents and change heading types

* Revert language section

* Move table of contents and delete first two entries

* Fix broken links
2024-08-27 16:49:43 +05:30
KameronKeller
6bd9d4c480 Fix/2377 Fix humanizeDate so that it always returns the date it is passed (#2378)
* Add tests for humanizeDate and relativeDate

* Add fix to humanizeDate

* Fix test description
2024-08-27 16:16:35 +05:30
Mateusz Pietryga
a38d09a117 feat: Store client certificate paths in collection settings as relative to collection and display them the UI. (#2421)
#2420
2024-08-27 16:09:19 +05:30
Anoop M D
82985d1b43 chore: updated tests 2024-08-27 14:38:44 +05:30
Anoop M D
d34d3a45ff chore: updated test 2024-08-27 14:28:20 +05:30
Anoop M D
25ccb38202 chore: fix lint issue 2024-08-27 14:21:41 +05:30
Anoop M D
5f6a5f59b1 chore: updated check for js sandbox libs 2024-08-27 14:20:50 +05:30
Anoop M D
9e5148f032 fix(#2767): Fix serilization issues of bigint in json body (#2773)
Fix serilization issues of bigint in json body
---------

Co-authored-by: lohit <lohit.jiddimani@gmail.com>
2024-08-27 14:12:56 +05:30
57 changed files with 769 additions and 339 deletions

View File

@@ -39,7 +39,7 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
[Download Bruno](https://www.usebruno.com/downloads)
📢 Sehen Sie sich unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
![bruno](/assets/images/landing-2.png) <br /><br />
@@ -48,13 +48,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Sie können die [Golden Edition](https://www.usebruno.com/pricing) vorbestellen ~~$19~~ **$9** ! <br/>
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
Sie können Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
Du kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
```sh
# Auf Mac via Homebrew
@@ -123,11 +123,11 @@ Oder einer Versionskontrolle deiner Wahl
### Unterstützung ❤️
Wuff! Wenn du dieses Projekt magst, klick den ⭐ Button !!
Wuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !!
### Teile Erfahrungsberichte 📣
Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
Wenn Bruno dir und in deinem Team bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte in unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
### Bereitstellung in neuen Paket-Managern

View File

@@ -5,6 +5,7 @@ const StyledWrapper = styled.div`
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere;
flex: 1 1 0;
}

View File

@@ -67,7 +67,7 @@ if (!SERVER_RENDERED) {
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()'
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.sleep(ms)'
];
@@ -333,6 +333,7 @@ export default class CodeEditor extends React.Component {
className="h-full w-full flex flex-col relative"
aria-label="Code Editor"
font={this.props.font}
fontSize={this.props.fontSize}
ref={(node) => {
this._node = node;
}}

View File

@@ -10,8 +10,9 @@ import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'path';
import slash from 'utils/common/slash';
import { isWindowsOS } from 'utils/common/platform';
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
@@ -67,7 +68,15 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
});
const getFile = (e) => {
e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path);
if (e.files?.[0]?.path) {
let relativePath;
if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, e.files[0].path));
} else {
relativePath = path.posix.relative(root, e.files[0].path);
}
formik.setFieldValue(e.name, relativePath);
}
};
const resetFileInputFields = () => {
@@ -102,10 +111,14 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
: clientCertConfig.map((clientCert) => (
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex items-center">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>

View File

@@ -46,6 +46,7 @@ const Docs = ({ collection }) => {
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
@@ -104,7 +104,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
<Tooltip
<InfoTip
text={`
<div>
<ul>
@@ -114,7 +114,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
</ul>
</div>
`}
tooltipId="request-var"
infotipId="request-var"
/>
</label>
<div className="flex items-center">
@@ -336,4 +336,4 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
);
};
export default ProxySettings;
export default ProxySettings;

View File

@@ -52,6 +52,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>
<div className="flex-1 mt-6">
@@ -64,6 +65,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>

View File

@@ -36,6 +36,7 @@ const Tests = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<div className="mt-6">

View File

@@ -95,6 +95,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': {
return (
<ClientCertSettings
root={collection.pathname}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}

View File

@@ -47,6 +47,7 @@ const Documentation = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}

View File

@@ -54,6 +54,7 @@ const Script = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
@@ -66,6 +67,7 @@ const Script = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>

View File

@@ -37,6 +37,7 @@ const Tests = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<div className="mt-6">

View File

@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -82,14 +82,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import { Tooltip as ReactInfoTip } from 'react-tooltip';
const Tooltip = ({ text, tooltipId }) => {
const InfoTip = ({ text, infotipId }) => {
return (
<>
<svg
tabIndex="-1"
id={tooltipId}
id={infotipId}
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
@@ -17,9 +17,9 @@ const Tooltip = ({ text, tooltipId }) => {
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
<ReactTooltip anchorId={tooltipId} html={text} />
<ReactInfoTip anchorId={infotipId} html={text} />
</>
);
};
export default Tooltip;
export default InfoTip;

View File

@@ -10,6 +10,8 @@ import {
} from 'providers/ReduxStore/slices/notifications';
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
import { useTheme } from 'providers/Theme';
const PAGE_SIZE = 5;
@@ -20,6 +22,7 @@ const Notifications = () => {
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -85,21 +88,22 @@ const Notifications = () => {
return (
<StyledWrapper>
<a
title="Notifications"
className="relative cursor-pointer"
onClick={() => {
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
>
<IconBell
size={18}
strokeWidth={1.5}
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
/>
{unreadNotifications.length > 0 && (
<span className="notification-count text-xs">{unreadNotifications.length}</span>
)}
<ToolHint text="Notifications" toolhintId="Notifications" offset={8} >
<IconBell
size={18}
strokeWidth={1.5}
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
/>
{unreadNotifications.length > 0 && (
<span className="notification-count text-xs">{unreadNotifications.length}</span>
)}
</ToolHint>
</a>
{showNotificationsModal && (
@@ -129,9 +133,8 @@ const Notifications = () => {
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li
key={notification.id}
className={`p-4 flex flex-col justify-center ${
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
className={`p-4 flex flex-col justify-center ${selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
onClick={handleNotificationItemClick(notification)}
>
<div className="notification-title w-full">{notification?.title}</div>
@@ -141,9 +144,8 @@ const Notifications = () => {
</ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
className={`pl-2 pr-2 py-3 select-none ${pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handlePrev}
>
{'Prev'}
@@ -159,9 +161,8 @@ const Notifications = () => {
</div>
</div>
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
className={`pl-2 pr-2 py-3 select-none ${pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNext}
>
{'Next'}

View File

@@ -9,17 +9,25 @@ const Font = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '14'));
const handleInputChange = (event) => {
const handleCodeFontChange = (event) => {
setCodeFont(event.target.value);
};
const handleCodeFontSizeChange = (event) => {
// Restrict to min/max value
const clampedSize = Math.max(1, Math.min(event.target.value, 32));
setCodeFontSize(clampedSize);
};
const handleSave = () => {
dispatch(
savePreferences({
...preferences,
font: {
codeFont
codeFont,
codeFontSize
}
})
).then(() => {
@@ -29,17 +37,33 @@ const Font = ({ close }) => {
return (
<StyledWrapper>
<label className="block font-medium">Code Editor Font</label>
<input
type="text"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleInputChange}
defaultValue={codeFont}
/>
<div className="flex flex-row gap-2 w-full">
<div className="w-4/5">
<label className="block font-medium">Code Editor Font</label>
<input
type="text"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleCodeFontChange}
defaultValue={codeFont}
/>
</div>
<div className="w-1/5">
<label className="block font-medium">Font Size</label>
<input
type="number"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
inputMode="numeric"
onChange={handleCodeFontSizeChange}
defaultValue={codeFontSize}
/>
</div>
</div>
<div className="mt-10">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
width: 100px;
}
.textbox {
@@ -20,6 +20,12 @@ const StyledWrapper = styled.div`
outline: none !important;
}
}
.system-proxy-settings {
label {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`;
export default StyledWrapper;

View File

@@ -11,14 +11,17 @@ import { useState } from 'react';
const ProxySettings = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
const dispatch = useDispatch();
console.log(preferences);
const proxySchema = Yup.object({
enabled: Yup.boolean(),
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: true,
is: 'on',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
@@ -31,7 +34,7 @@ const ProxySettings = ({ close }) => {
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: true,
is: 'on',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
@@ -54,7 +57,7 @@ const ProxySettings = ({ close }) => {
const formik = useFormik({
initialValues: {
enabled: preferences.proxy.enabled || false,
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || 0,
@@ -94,7 +97,7 @@ const ProxySettings = ({ close }) => {
useEffect(() => {
formik.setValues({
enabled: preferences.proxy.enabled || false,
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || '',
@@ -109,188 +112,256 @@ const ProxySettings = ({ close }) => {
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
Mode
</label>
<div className="flex items-center">
<label className="flex items-center">
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
name="mode"
value="false"
checked={formik.values.mode === 'off'}
onChange={(e) => {
formik.setFieldValue('mode', 'off');
}}
className="mr-1 cursor-pointer"
/>
HTTP
Off
</label>
<label className="flex items-center ml-4">
<label className="flex items-center ml-4 cursor-pointer">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
name="mode"
value="true"
checked={formik.values.mode === 'on'}
onChange={(e) => {
formik.setFieldValue('mode', 'on');
}}
className="mr-1 cursor-pointer"
/>
HTTPS
On
</label>
<label className="flex items-center ml-4">
<label className="flex items-center ml-4 cursor-pointer">
<input
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
name="mode"
value="system"
checked={formik.values.mode === 'system'}
onChange={formik.handleChange}
className="mr-1"
className="mr-1 cursor-pointer"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
System Proxy
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
{formik?.values?.mode === 'system' ? (
<div className="mb-3 pt-1 text-muted system-proxy-settings">
<small>
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
Please refer to your OS documentation to change these values.
</small>
<div className="flex flex-col justify-start items-start pt-2">
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="http_proxy">
http_proxy
</label>
<div className="opacity-80">{http_proxy || '-'}</div>
</div>
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="https_proxy">
https_proxy
</label>
<div className="opacity-80">{https_proxy || '-'}</div>
</div>
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="no_proxy">
no_proxy
</label>
<div className="opacity-80">{no_proxy || '-'}</div>
</div>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
) : null}
{formik?.values?.mode === 'on' ? (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTP
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTPS
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
className="outline-none w-[10.5rem] bg-transparent"
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
<input
id="auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
className="outline-none w-[10.5rem] bg-transparent"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
</>
) : null}
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save

View File

@@ -31,6 +31,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
: get(item, 'request.body.graphql.variables');
const { displayedTheme } = useTheme();
const [schema, setSchema] = useState(null);
const preferences = useSelector((state) => state.app.preferences);
useEffect(() => {
onSchemaLoad(schema);
@@ -71,6 +72,8 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
);
}

View File

@@ -33,6 +33,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
value={variables || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}

View File

@@ -4,6 +4,8 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
flex: 1 1 0;
}

View File

@@ -211,6 +211,8 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full w-full flex flex-col relative"
aria-label="Query Editor"
font={this.props.font}
fontSize={this.props.fontSize}
ref={(node) => {
this._node = node;
}}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
@@ -175,7 +175,7 @@ const QueryParams = ({ item, collection }) => {
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<Tooltip
<InfoTip
text={`
<div>
Path variables are automatically added whenever the
@@ -186,7 +186,7 @@ const QueryParams = ({ item, collection }) => {
</code>
</div>
`}
tooltipId="path-param-tooltip"
infotipId="path-param-InfoTip"
/>
</div>
<table>

View File

@@ -33,18 +33,18 @@ const Wrapper = styled.div`
top: 1px;
}
.tooltip {
.infotip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip:hover .tooltiptext {
.infotip:hover .infotiptext {
visibility: visible;
opacity: 1;
}
.tooltiptext {
.infotiptext {
visibility: hidden;
width: auto;
background-color: ${(props) => props.theme.requestTabs.active.bg};
@@ -62,7 +62,7 @@ const Wrapper = styled.div`
white-space: nowrap;
}
.tooltiptext::after {
.infotiptext::after {
content: '';
position: absolute;
top: 100%;

View File

@@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div
className="tooltip mx-3"
className="infotip mr-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;
@@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
size={22}
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
/>
<span className="tooltiptext text-xs">
<span className="infotiptext text-xs">
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>

View File

@@ -50,6 +50,7 @@ const RequestBody = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}

View File

@@ -47,6 +47,7 @@ const Script = ({ item, collection }) => {
value={requestScript || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onRequestScriptEdit}
mode="javascript"
onRun={onRun}
@@ -60,6 +61,7 @@ const Script = ({ item, collection }) => {
value={responseScript || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onResponseScriptEdit}
mode="javascript"
onRun={onRun}

View File

@@ -34,6 +34,7 @@ const Tests = ({ item, collection }) => {
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}

View File

@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -83,14 +83,14 @@ const VarsTable = ({ item, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

@@ -2,4 +2,4 @@ import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -4,6 +4,7 @@ import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
@@ -51,14 +52,20 @@ const CollectionToolBar = ({ collection }) => {
<span className="mr-2">
<JsSandboxMode collection={collection} />
</span>
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
<span className="mr-3">
<ToolHint text="Runner" toolhintId="RunnnerToolhintId" place='bottom'>
<IconRun className="cursor-pointer" size={18} strokeWidth={1.5} onClick={handleRun} />
</ToolHint>
</span>
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
<ToolHint text="Variables" toolhintId="VariablesToolhintId">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</ToolHint>
</span>
<span className="mr-3">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
<ToolHint text="Collection Settings" toolhintId="CollectionSettingsToolhintId">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
</ToolHint>
</span>
<EnvironmentSelector collection={collection} />
</div>

View File

@@ -2,7 +2,7 @@ import { IconFilter, IconX } from '@tabler/icons';
import React, { useMemo } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import { Tooltip as ReactInfotip } from 'react-tooltip';
const QueryResultFilter = ({ filter, onChange, mode }) => {
const inputRef = useRef(null);
@@ -19,7 +19,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
};
const tooltipText = useMemo(() => {
const infotipText = useMemo(() => {
if (mode.includes('json')) {
return 'Filter with JSONPath';
}
@@ -49,7 +49,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
}
>
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
{infotipText && !isExpanded && <ReactInfotip anchorId={'request-filter-icon'} html={infotipText} />}
<input
ref={inputRef}
type="text"

View File

@@ -81,6 +81,7 @@ const QueryResultPreview = ({
<CodeEditor
collection={collection}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
onRun={onRun}
value={formattedData}

View File

@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CloneCollection = ({ onClose, collection }) => {
@@ -126,9 +126,9 @@ const CloneCollection = ({ onClose, collection }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip
<InfoTip
text="This folder will be created under the selected location"
tooltipId="collection-folder-name-tooltip"
infotipId="collection-folder-name-infotip"
/>
</label>
<input

View File

@@ -53,6 +53,7 @@ const CodeView = ({ language, item }) => {
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
mode={lang}
/>

View File

@@ -65,7 +65,7 @@ const Wrapper = styled.div`
div.dropdown-item.delete-item {
color: ${(props) => props.theme.colors.danger};
&:hover {
background-color: ${(props) => props.theme.colors.bg.danger};
background-color: ${(props) => props.theme.colors.bg.danger} !important;
color: white;
}
}

View File

@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CreateCollection = ({ onClose }) => {
@@ -119,9 +119,9 @@ const CreateCollection = ({ onClose }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip
<InfoTip
text="This folder will be created under the selected location"
tooltipId="collection-folder-name-tooltip"
infotipId="collection-folder-name-infotip"
/>
</label>
<input

View File

@@ -4,6 +4,7 @@ import StyledWrapper from './StyledWrapper';
import GitHubButton from 'react-github-btn';
import Preferences from 'components/Preferences';
import Cookies from 'components/Cookies';
import ToolHint from 'components/ToolHint';
import GoldenEdition from './GoldenEdition';
import { useState, useEffect } from 'react';
@@ -95,28 +96,30 @@ const Sidebar = () => {
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
<div className="flex items-center ml-1 text-xs ">
<a
title="Preferences"
className="mr-2 cursor-pointer hover:text-gray-700"
onClick={() => dispatch(showPreferences(true))}
>
<IconSettings size={18} strokeWidth={1.5} />
<a className="mr-2 cursor-pointer" onClick={() => dispatch(showPreferences(true))}>
<ToolHint text="Preferences" toolhintId="Preferences" effect='float' place='top-start' offset={8}>
<IconSettings size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a
title="Cookies"
className="mr-2 cursor-pointer hover:text-gray-700"
className="mr-2 cursor-pointer"
onClick={() => setCookiesOpen(true)}
>
<IconCookie size={18} strokeWidth={1.5} />
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
<IconCookie size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a
title="Golden Edition"
className="mr-2 cursor-pointer hover:text-gray-700"
className="mr-2 cursor-pointer"
onClick={() => setGoldenEditonOpen(true)}
>
<IconHeart size={18} strokeWidth={1.5} />
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8} >
<IconHeart size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a>
<Notifications />
</a>
<Notifications />
</div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
{/* This will get moved to home page */}
@@ -126,10 +129,10 @@ const Sidebar = () => {
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
</div>
</div>
</div>
@@ -137,7 +140,7 @@ const Sidebar = () => {
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
</StyledWrapper>
</StyledWrapper >
);
};

View File

@@ -0,0 +1,8 @@
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: ${(props) => props.theme.sidebar.badge};
color: ${(props) => props.theme.text};
`;
export default Wrapper;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Tooltip as ReactToolHint } from 'react-tooltip';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
const ToolHint = ({
text,
toolhintId,
children,
tooltipStyle = {},
place = 'top',
offset,
theme = null
}) => {
const { theme: contextTheme } = useTheme();
const appliedTheme = theme || contextTheme;
const toolhintBackgroundColor = appliedTheme?.sidebar.badge.bg || 'black';
const toolhintTextColor = appliedTheme?.text || 'white';
const combinedToolhintStyle = {
...tooltipStyle,
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
backgroundColor: toolhintBackgroundColor,
color: toolhintTextColor
};
return (
<>
<span id={toolhintId}>{children}</span>
<StyledWrapper theme={appliedTheme}>
<ReactToolHint
anchorId={toolhintId}
html={text}
className="toolhint"
offset={offset}
place={place}
noArrow={true}
style={combinedToolhintStyle}
/>
</StyledWrapper>
</>
);
};
export default ToolHint;

View File

@@ -1,5 +1,10 @@
import { useEffect } from 'react';
import { showPreferences, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
import {
showPreferences,
updateCookies,
updatePreferences,
updateSystemProxyEnvVariables
} from 'providers/ReduxStore/slices/app';
import {
brunoConfigUpdateEvent,
collectionAddDirectoryEvent,
@@ -136,6 +141,10 @@ const useIpcEvents = () => {
dispatch(updatePreferences(val));
});
const removeSystemProxyEnvUpdatesListener = ipcRenderer.on('main:load-system-proxy-env', (val) => {
dispatch(updateSystemProxyEnvVariables(val));
});
const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {
dispatch(updateCookies(val));
});
@@ -155,6 +164,7 @@ const useIpcEvents = () => {
removeShowPreferencesListener();
removePreferencesUpdatesListener();
removeCookieUpdateListener();
removeSystemProxyEnvUpdatesListener();
};
}, [isElectron]);
};

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.26.2'
version: '1.27.0'
}
});
};

View File

@@ -9,7 +9,7 @@ import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
export const HotkeysContext = React.createContext();
@@ -154,7 +154,41 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
// close all tabs
// Switch to the previous tab
useEffect(() => {
Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
dispatch(
switchTab({
direction: 'pageup'
})
);
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
};
}, [dispatch]);
// Switch to the next tab
useEffect(() => {
Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
dispatch(
switchTab({
direction: 'pagedown'
})
);
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
};
}, [dispatch]);
// Close all tabs
useEffect(() => {
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);

View File

@@ -27,7 +27,8 @@ const initialState = {
}
},
cookies: [],
taskQueue: []
taskQueue: [],
systemProxyEnvVariables: {}
};
export const appSlice = createSlice({
@@ -72,6 +73,9 @@ export const appSlice = createSlice({
},
removeAllTasksFromQueue: (state) => {
state.taskQueue = [];
},
updateSystemProxyEnvVariables: (state, action) => {
state.systemProxyEnvVariables = action.payload;
}
}
});
@@ -89,7 +93,8 @@ export const {
updateCookies,
insertTaskIntoQueue,
removeTaskFromQueue,
removeAllTasksFromQueue
removeAllTasksFromQueue,
updateSystemProxyEnvVariables
} = appSlice.actions;
export const savePreferences = (preferences) => (dispatch, getState) => {

View File

@@ -48,6 +48,26 @@ export const tabsSlice = createSlice({
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
},
switchTab: (state, action) => {
if (!state.tabs || !state.tabs.length) {
state.activeTabUid = null;
return;
}
const direction = action.payload.direction;
const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);
let toBeActivatedTabIndex = 0;
if (direction == 'pageup') {
toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;
} else if (direction == 'pagedown') {
toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;
}
state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;
},
updateRequestPaneTabWidth: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
@@ -111,6 +131,7 @@ export const tabsSlice = createSlice({
export const {
addTab,
focusTab,
switchTab,
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,

View File

@@ -149,7 +149,9 @@ export const relativeDate = (dateString) => {
};
export const humanizeDate = (dateString) => {
const date = new Date(dateString);
// See this discussion for why .split is necessary
// https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off
const date = new Date(dateString.split('-'));
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',

View File

@@ -1,6 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
import { normalizeFileName, startsWith } from './index';
import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
describe('common utils', () => {
describe('normalizeFileName', () => {
@@ -49,4 +49,50 @@ describe('common utils', () => {
expect(startsWith('foo', 'foo')).toBe(true);
});
});
describe('humanizeDate', () => {
it('should return a date string in the en-US locale', () => {
expect(humanizeDate('2024-03-17')).toBe('March 17, 2024');
});
it('should return invalid date if the date is invalid', () => {
expect(humanizeDate('9999-99-99')).toBe('Invalid Date');
});
});
describe('relativeDate', () => {
it('should return few seconds ago', () => {
expect(relativeDate(new Date())).toBe('Few seconds ago');
});
it('should return minutes ago', () => {
let date = new Date();
date.setMinutes(date.getMinutes() - 30);
expect(relativeDate(date)).toBe('30 minutes ago');
});
it('should return hours ago', () => {
let date = new Date();
date.setHours(date.getHours() - 10);
expect(relativeDate(date)).toBe('10 hours ago');
});
it('should return days ago', () => {
let date = new Date();
date.setDate(date.getDate() - 5);
expect(relativeDate(date)).toBe('5 days ago');
});
it('should return weeks ago', () => {
let date = new Date();
date.setDate(date.getDate() - 8);
expect(relativeDate(date)).toBe('1 week ago');
});
it('should return months ago', () => {
let date = new Date();
date.setDate(date.getDate() - 60);
expect(relativeDate(date)).toBe('2 months ago');
});
});
});

View File

@@ -18,16 +18,17 @@ const hasLength = (str) => {
};
export const parseQueryParams = (query) => {
if (!query || !query.length) {
try {
if (!query || !query.length) {
return [];
}
return Array.from(new URLSearchParams(query.split('#')[0]).entries())
.map(([name, value]) => ({ name, value }));
} catch (error) {
console.error('Error parsing query params:', error);
return [];
}
let params = query.split('&').map((param) => {
let [name, value = ''] = param.split('=');
return { name, value };
});
return filter(params, (p) => hasLength(p.name));
};
export const parsePathParams = (url) => {

View File

@@ -49,6 +49,23 @@ describe('Url Utils - parseQueryParams', () => {
{ name: 'b', value: '2' }
]);
});
it('should parse query with "=" character - case 9', () => {
const params = parseQueryParams('a=1&b={color=red,size=large}&c=3');
expect(params).toEqual([
{ name: 'a', value: '1' },
{ name: 'b', value: '{color=red,size=large}' },
{ name: 'c', value: '3' }
]);
});
it('should parse query with fragment - case 10', () => {
const params = parseQueryParams('a=1&b=2#I-AM-FRAGMENT');
expect(params).toEqual([
{ name: 'a', value: '1' },
{ name: 'b', value: '2' }
]);
});
});
describe('Url Utils - parsePathParams', () => {

View File

@@ -1,5 +1,5 @@
{
"version": "v1.26.2",
"version": "v1.27.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@@ -3,7 +3,7 @@ const path = require('path');
const isDev = require('electron-is-dev');
if (isDev) {
if (!fs.existsSync('./src/sandbox/bundle-browser-rollup.js')) {
if(!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {
console.log('JS Sandbox libraries have not been bundled yet');
console.log('Please run the below command \nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');
throw new Error('JS Sandbox libraries have not been bundled yet');

View File

@@ -49,7 +49,9 @@ const checkConnection = (host, port) =>
*/
function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */
const instance = axios.create();
const instance = axios.create({
proxy: false
});
instance.interceptors.request.use(async (config) => {
const url = URL.parse(config.url);

View File

@@ -165,51 +165,92 @@ const configureRequest = async (
}
}
// proxy configuration
let proxyConfig = get(brunoConfig, 'proxy', {});
let proxyEnabled = get(proxyConfig, 'enabled', 'global');
if (proxyEnabled === 'global') {
/**
* Proxy configuration
*
* Preferences proxyMode has three possible values: on, off, system
* Collection proxyMode has three possible values: true, false, global
*
* When collection proxyMode is true, it overrides the app-level proxy settings
* When collection proxyMode is false, it ignores the app-level proxy settings
* When collection proxyMode is global, it uses the app-level proxy settings
*
* Below logic calculates the proxyMode and proxyConfig to be used for the request
*/
let proxyMode = 'off';
let proxyConfig = {};
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global');
if (collectionProxyEnabled === true) {
proxyConfig = collectionProxyConfig;
proxyMode = 'on';
} else if (collectionProxyEnabled === 'global') {
proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyEnabled = get(proxyConfig, 'enabled', false);
proxyMode = get(proxyConfig, 'mode', 'off');
}
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (proxyEnabled === true && shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
request.httpsAgent = new SocksProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new SocksProxyAgent(proxyUri);
} else {
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
}
if (socksEnabled) {
request.httpsAgent = new SocksProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new SocksProxyAgent(proxyUri);
} else {
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
if (shouldUseSystemProxy) {
try {
if (http_proxy?.length) {
new URL(http_proxy);
request.httpAgent = new HttpProxyAgent(http_proxy);
}
} catch (error) {
throw new Error('Invalid system http_proxy');
}
try {
if (https_proxy?.length) {
new URL(https_proxy);
request.httpsAgent = new PatchedHttpsProxyAgent(
https_proxy,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
}
} catch (error) {
throw new Error('Invalid system https_proxy');
}
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
const axiosInstance = makeAxiosInstance();
if (request.oauth2) {

View File

@@ -1,5 +1,5 @@
const { ipcMain } = require('electron');
const { getPreferences, savePreferences } = require('../store/preferences');
const { getPreferences, savePreferences, preferencesUtil } = require('../store/preferences');
const { isDirectory } = require('../utils/filesystem');
const { openCollection } = require('../app/collections');
``;
@@ -9,6 +9,10 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
const preferences = getPreferences();
mainWindow.webContents.send('main:load-preferences', preferences);
const systemProxyVars = preferencesUtil.getSystemProxyEnvVariables();
const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {};
mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy });
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();

View File

@@ -1,6 +1,6 @@
const Yup = require('yup');
const Store = require('electron-store');
const { get } = require('lodash');
const { get, merge } = require('lodash');
/**
* The preferences are stored in the electron store 'preferences.json'.
@@ -23,10 +23,11 @@ const defaultPreferences = {
timeout: 0
},
font: {
codeFont: 'default'
codeFont: 'default',
codeFontSize: 14
},
proxy: {
enabled: false,
mode: 'off',
protocol: 'http',
hostname: '',
port: null,
@@ -54,10 +55,11 @@ const preferencesSchema = Yup.object().shape({
timeout: Yup.number()
}),
font: Yup.object().shape({
codeFont: Yup.string().nullable()
codeFont: Yup.string().nullable(),
codeFontSize: Yup.number().min(1).max(32).nullable()
}),
proxy: Yup.object({
enabled: Yup.boolean(),
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(1).max(65535).nullable(),
@@ -79,10 +81,22 @@ class PreferencesStore {
}
getPreferences() {
return {
...defaultPreferences,
...this.store.get('preferences')
};
let preferences = this.store.get('preferences', {});
// This to support the old preferences format
// In the old format, we had a proxy.enabled flag
// In the new format, this maps to proxy.mode = 'on'
if (preferences?.proxy?.enabled) {
preferences.proxy.mode = 'on';
}
// Delete the proxy.enabled property if it exists, regardless of its value
// This is a part of migration to the new preferences format
if (preferences?.proxy && 'enabled' in preferences.proxy) {
delete preferences.proxy.enabled;
}
return merge({}, defaultPreferences, preferences);
}
savePreferences(newPreferences) {
@@ -134,6 +148,14 @@ const preferencesUtil = {
},
shouldSendCookies: () => {
return get(getPreferences(), 'request.sendCookies', true);
},
getSystemProxyEnvVariables: () => {
const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
return {
http_proxy: http_proxy || HTTP_PROXY,
https_proxy: https_proxy || HTTPS_PROXY,
no_proxy: no_proxy || NO_PROXY
};
}
};

View File

@@ -25,13 +25,11 @@ auth:bearer {
body:json {
{
"hello": 990531470713421825
}
}
body:text {
{
"hello": 990531470713421825
"hello": 990531470713421825,
"decimal": 1.0,
"decimal2": 1.00,
"decimal3": 1.00200,
"decimal4": 0.00
}
}
@@ -39,6 +37,6 @@ assert {
res.status: eq 200
}
script:pre-request {
bru.setVar("foo", "foo-world-2");
tests {
// todo: add tests once lossless json echo server is ready
}

View File

@@ -43,14 +43,34 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
![bruno](assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
## Golden Edition ✨
Majority of our features are free and open source.
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19**! <br/>
### Installation
## Table of Contents
- [Installation](#installation)
- [Features](#features)
- [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
- [Collaborate via Git 👩‍💻🧑‍💻](#collaborate-via-git-)
- [Sponsors](#sponsors)
- [Gold Sponsors](#gold-sponsors)
- [Silver Sponsors](#silver-sponsors)
- [Bronze Sponsors](#bronze-sponsors)
- [Important Links 📌](#important-links-)
- [Showcase 🎥](#showcase-)
- [Support ❤️](#support-%EF%B8%8F)
- [Share Testimonials 📣](#share-testimonials-)
- [Publishing to New Package Managers](#publishing-to-new-package-managers)
- [Stay in touch 🌐](#stay-in-touch-)
- [Trademark](#trademark)
- [Contribute 👩‍💻🧑‍💻](#contribute-)
- [Authors](#authors)
- [License 📄](#license-)
## Installation
Bruno is available as binary download [on our website](https://www.usebruno.com/downloads) for Mac, Windows and Linux.
@@ -86,6 +106,8 @@ sudo apt update
sudo apt install bruno
```
## Features
### Run across multiple platforms 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
@@ -96,7 +118,7 @@ Or any version control system of your choice
![bruno](assets/images/version-control.png) <br /><br />
### Sponsors
## Sponsors
#### Gold Sponsors
@@ -112,7 +134,7 @@ Or any version control system of your choice
<img src="assets/images/sponsors/zuplo.png" width="120"/>
</a>
### Important Links 📌
## Important Links 📌
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
@@ -123,32 +145,32 @@ Or any version control system of your choice
- [Download](https://www.usebruno.com/downloads)
- [GitHub Sponsors](https://github.com/sponsors/helloanoop).
### Showcase 🎥
## Showcase 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Support ❤️
## Support ❤️
If you like Bruno and want to support our opensource work, consider sponsoring us via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
### Share Testimonials 📣
## Share Testimonials 📣
If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)
### Publishing to New Package Managers
## Publishing to New Package Managers
Please see [here](publishing.md) for more information.
### Stay in touch 🌐
## Stay in touch 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Trademark
## Trademark
**Name**
@@ -158,13 +180,13 @@ Please see [here](publishing.md) for more information.
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Contribute 👩‍💻🧑‍💻
## Contribute 👩‍💻🧑‍💻
I am happy that you are looking to improve bruno. Please check out 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.
### Authors
## Authors
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
@@ -172,6 +194,6 @@ Even if you are not able to make contributions via code, please don't hesitate t
</a>
</div>
### License 📄
## License 📄
[MIT](license.md)