Compare commits

...

16 Commits

Author SHA1 Message Date
Anoop M D
5713e19c23 Collection Variables Support (#2963)
* Update package.json

* Update package.json

* Update package.json

* Update package.json

* draft: collection varaibles

* reverted jest command

* precedence update

* feat: updates

* feat: updates

* feat: updates

* feat: pre-vars values as strings

* feat: updates

---------

Co-authored-by: lohit <lohit.jiddimani@gmail.com>
Co-authored-by: lohit <lohxt.space@gmail.com>
2024-09-03 21:02:22 +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
77 changed files with 1104 additions and 359 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

53
package-lock.json generated
View File

@@ -50,6 +50,7 @@
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -670,6 +671,7 @@
},
"node_modules/@babel/compat-data": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -677,6 +679,7 @@
},
"node_modules/@babel/core": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -740,6 +743,7 @@
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.25.2",
@@ -754,6 +758,7 @@
},
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -761,6 +766,7 @@
},
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"dev": true,
"license": "ISC"
},
"node_modules/@babel/helper-create-class-features-plugin": {
@@ -839,6 +845,7 @@
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.24.7",
@@ -905,6 +912,7 @@
},
"node_modules/@babel/helper-simple-access": {
"version": "7.24.7",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.24.7",
@@ -942,6 +950,7 @@
},
"node_modules/@babel/helper-validator-option": {
"version": "7.24.8",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -962,6 +971,7 @@
},
"node_modules/@babel/helpers": {
"version": "7.25.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.0",
@@ -3644,37 +3654,6 @@
"node": ">=12"
}
},
"node_modules/@n8n/vm2": {
"version": "3.9.25",
"resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.25.tgz",
"integrity": "sha512-qoGLFzyHBW7HKpwXkl05QKsIh3GkDw6lOiTOWYlUDnOIQ1b7EgM+O5EMjrMGy7r+kz52+Q7o6GLxBIcxVI8rEg==",
"license": "MIT",
"peer": true,
"dependencies": {
"acorn": "^8.7.0",
"acorn-walk": "^8.2.0"
},
"bin": {
"vm2": "bin/vm2"
},
"engines": {
"node": ">=18.10",
"pnpm": ">=9.6"
}
},
"node_modules/@n8n/vm2/node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@next/env": {
"version": "12.3.3",
"license": "MIT"
@@ -4751,6 +4730,7 @@
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -4759,6 +4739,7 @@
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -4767,6 +4748,7 @@
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/minimatch": {
@@ -6240,6 +6222,7 @@
},
"node_modules/browserslist": {
"version": "4.23.3",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -7238,6 +7221,7 @@
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -8477,6 +8461,7 @@
},
"node_modules/electron-to-chromium": {
"version": "1.5.11",
"dev": true,
"license": "ISC"
},
"node_modules/electron-util": {
@@ -9438,6 +9423,7 @@
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -13186,6 +13172,7 @@
},
"node_modules/node-releases": {
"version": "2.0.18",
"dev": true,
"license": "MIT"
},
"node_modules/node-vault": {
@@ -16201,6 +16188,7 @@
},
"node_modules/semver": {
"version": "6.3.1",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -17663,6 +17651,7 @@
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -18633,7 +18622,7 @@
},
"packages/bruno-electron": {
"name": "bruno",
"version": "v1.26.1",
"version": "v1.27.0",
"dependencies": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",

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,6 +67,7 @@ if (!SERVER_RENDERED) {
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.sleep(ms)'
];
@@ -332,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">

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

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,162 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
import {
addCollectionVar,
deleteCollectionVar,
updateCollectionVar
} from 'providers/ReduxStore/slices/collections/index';
const VarsTable = ({ collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addVar = () => {
dispatch(
addCollectionVar({
collectionUid: collection.uid,
type: varType
})
);
};
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
const value = e.target.value;
if (variableNameRegex.test(value) === false) {
toast.error(
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
);
return;
}
_var.name = value;
break;
}
case 'value': {
_var.value = e.target.value;
break;
}
case 'enabled': {
_var.enabled = e.target.checked;
break;
}
}
dispatch(
updateCollectionVar({
type: varType,
var: _var,
collectionUid: collection.uid
})
);
};
const handleRemoveVar = (_var) => {
dispatch(
deleteCollectionVar({
type: varType,
varUid: _var.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{varType === 'request' ? (
<td>
<div className="flex items-center">
<span>Value</span>
<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>
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
? vars.map((_var) => {
return (
<tr key={_var.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={_var.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, _var, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleVarChange(
{
target: {
value: newValue
}
},
_var,
'value'
)
}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={_var.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
+ Add
</button>
</StyledWrapper>
);
};
export default VarsTable;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Vars;

View File

@@ -16,6 +16,7 @@ import Docs from './Docs';
import Presets from './Presets';
import Info from './Info';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
@@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
case 'headers': {
return <Headers collection={collection} />;
}
case 'vars': {
return <Vars collection={collection} />;
}
case 'auth': {
return <Auth collection={collection} />;
}
@@ -95,6 +99,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': {
return (
<ClientCertSettings
root={collection.pathname}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}
@@ -122,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
Vars
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
</div>

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

@@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
)
}
collection={collection}
item={folder}
/>
</td>
<td>

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>
)}
@@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
)
}
collection={collection}
item={folder}
/>
</td>
<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

@@ -191,6 +191,7 @@ const AssertionRow = ({
}
onRun={handleRun}
collection={collection}
item={item}
/>
) : (
<input type="text" className="cursor-default" disabled />

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>
)}
@@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<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

@@ -17,8 +17,6 @@ const Timeline = ({ request, response }) => {
});
});
let requestData = safeStringifyJSON(request.data);
return (
<StyledWrapper className="pb-4 w-full">
<div>
@@ -33,9 +31,10 @@ const Timeline = ({ request, response }) => {
);
})}
{requestData ? (
{request.data ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data {requestData}
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
</pre>
) : null}
</div>

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

@@ -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

@@ -1302,6 +1302,71 @@ export const collectionsSlice = createSlice({
set(collection, 'root.request.headers', headers);
}
},
addCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (type === 'request') {
const vars = get(collection, 'root.request.vars.req', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
const vars = get(collection, 'root.request.vars.res', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.res', vars);
}
}
},
updateCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'root.request.vars.res', vars);
}
},
deleteCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.res', vars);
}
}
},
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
@@ -1694,6 +1759,9 @@ export const {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
addCollectionVar,
updateCollectionVar,
deleteCollectionVar,
updateCollectionAuthMode,
updateCollectionAuth,
updateCollectionRequestScript,

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

@@ -787,24 +787,25 @@ export const getTotalRequestCountInCollection = (collection) => {
};
export const getAllVariables = (collection, item) => {
const environmentVariables = getEnvironmentVariables(collection);
let requestVariables = {};
if (item?.request) {
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
}
const envVariables = getEnvironmentVariables(collection);
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
const pathParams = getPathParams(item);
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
return {
...environmentVariables,
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...collection.runtimeVariables,
...runtimeVariables,
pathParams: {
...pathParams
},
process: {
env: {
...collection.processEnvVariables
...processEnvVariables
}
}
};
@@ -831,14 +832,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
const mergeFolderLevelVars = (request, requestTreePath = []) => {
const mergeVars = (collection, requestTreePath = []) => {
let collectionVariables = {};
let folderVariables = {};
let requestVariables = {};
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
collectionVariables[_var.name] = _var.value;
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
requestVariables[_var.name] = _var.value;
folderVariables[_var.name] = _var.value;
}
});
} else {
@@ -850,6 +859,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
});
}
}
return requestVariables;
return {
collectionVariables,
folderVariables,
requestVariables
};
};

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

@@ -60,20 +60,6 @@ const runSingleRequest = async function (
request.data = form;
}
// run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVariables,
runtimeVariables,
collectionPath,
processEnvVars
);
}
// run pre request script
const requestScriptFile = compact([
get(collectionRoot, 'request.script.req'),
@@ -276,7 +262,7 @@ const runSingleRequest = async function (
console.log(
chalk.green(stripExtension(filename)) +
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
);
// run post-response vars

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

@@ -272,7 +272,7 @@ const configureRequest = async (
return axiosInstance;
};
const parseDataFromResponse = (response) => {
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
@@ -290,8 +290,10 @@ const parseDataFromResponse = (response) => {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
data = JSON.parse(data);
} catch {}
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch { }
return { data, dataBuffer };
};
@@ -317,20 +319,6 @@ const registerNetworkIpc = (mainWindow) => {
processEnvVars,
scriptingConfig
) => {
// run pre-request vars
const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVars,
runtimeVariables,
collectionPath,
processEnvVars
);
}
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
@@ -540,7 +528,7 @@ const registerNetworkIpc = (mainWindow) => {
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = responseTime;
@@ -701,7 +689,7 @@ const registerNetworkIpc = (mainWindow) => {
}
}
const { data } = parseDataFromResponse(response);
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
@@ -969,7 +957,7 @@ const registerNetworkIpc = (mainWindow) => {
response = await axiosInstance(request);
timeEnd = Date.now();
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = response.headers.get('request-duration');
@@ -1158,7 +1146,7 @@ const registerNetworkIpc = (mainWindow) => {
try {
const disposition = contentDispositionParser.parse(contentDisposition);
return disposition && disposition.parameters['filename'];
} catch (error) {}
} catch (error) { }
};
const getFileNameFromUrlPath = () => {

View File

@@ -12,15 +12,17 @@ const getContentType = (headers = {}) => {
return contentType;
};
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
envVariables = cloneDeep(envVariables);
// envVars can inturn have values as {{process.env.VAR_NAME}}
// so we need to interpolate envVars first with processEnvVars
forOwn(envVars, (value, key) => {
envVars[key] = interpolate(value, {
forOwn(envVariables, (value, key) => {
envVariables[key] = interpolate(value, {
process: {
env: {
...processEnvVars
@@ -36,7 +38,9 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
// runtimeVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
process: {
@@ -59,14 +63,6 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {}
}
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);

View File

@@ -1,7 +1,6 @@
const os = require('os');
const { get, each, filter, extend, compact } = require('lodash');
const decomment = require('decomment');
var JSONbig = require('json-bigint');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
@@ -45,73 +44,75 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeFolderLevelVars = (request, requestTreePath) => {
let folderReqVars = new Map();
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderReqVars.set(_var.name, _var.value);
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else if (i.uid === request.uid) {
} else {
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);
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
let requestReqVars = request?.vars?.req || [];
let requestReqVarsMap = new Map();
for (let _var of requestReqVars) {
if (_var.enabled) {
requestReqVarsMap.set(_var.name, _var.value);
}
}
mergedFolderReqVars.forEach((_var) => {
requestReqVarsMap.set(_var.name, _var.value);
});
request.vars.req = Array.from(requestReqVarsMap, ([name, value]) => ({
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
let folderResVars = new Map();
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderResVars.set(_var.name, _var.value);
resVars.set(_var.name, _var.value);
}
});
} else if (i.uid === request.uid) {
} else {
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);
resVars.set(_var.name, _var.value);
}
});
}
}
let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
let requestResVars = request?.vars?.res || [];
let requestResVarsMap = new Map();
for (let _var of requestResVars) {
if (_var.enabled) {
requestResVarsMap.set(_var.name, _var.value);
}
}
mergedFolderResVars.forEach((_var) => {
requestResVarsMap.set(_var.name, _var.value);
});
request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
@@ -315,7 +316,7 @@ const prepareRequest = (item, collection) => {
if (requestTreePath && requestTreePath.length > 0) {
mergeFolderLevelHeaders(request, requestTreePath);
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
mergeFolderLevelVars(request, requestTreePath);
mergeVars(collection, request, requestTreePath);
}
each(request.headers, (h) => {
@@ -342,16 +343,10 @@ const prepareRequest = (item, collection) => {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
let jsonBody;
try {
jsonBody = decomment(request?.body?.json);
axiosRequest.data = decomment(request?.body?.json);
} catch (error) {
jsonBody = request?.body?.json;
}
try {
axiosRequest.data = JSONbig.parse(jsonBody);
} catch (error) {
axiosRequest.data = jsonBody;
axiosRequest.data = request?.body?.json;
}
}
@@ -408,6 +403,9 @@ const prepareRequest = (item, collection) => {
}
axiosRequest.vars = request.vars;
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.assertions = request.assertions;
return axiosRequest;

View File

@@ -23,7 +23,8 @@ const defaultPreferences = {
timeout: 0
},
font: {
codeFont: 'default'
codeFont: 'default',
codeFontSize: 14
},
proxy: {
enabled: false,
@@ -54,7 +55,8 @@ 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(),

View File

@@ -6,7 +6,7 @@ describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
it('If request body is valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
const expected = { test: '{{someVar}}' };
const expected = '{\n"test": "{{someVar}}" \n}';
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});

View File

@@ -4,10 +4,12 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;
class Bru {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables) {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables) {
this.envVariables = envVariables || {};
this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
this.collectionVariables = collectionVariables || {};
this.folderVariables = folderVariables || {};
this.requestVariables = requestVariables || {};
this.collectionPath = collectionPath;
}
@@ -18,7 +20,9 @@ class Bru {
}
const combinedVars = {
...this.collectionVariables,
...this.envVariables,
...this.folderVariables,
...this.requestVariables,
...this.runtimeVariables,
process: {
@@ -71,7 +75,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
' Names must only contain alpha-numeric characters, "-", "_", "."'
' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -82,7 +86,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
' Names must only contain alpha-numeric characters, "-", "_", "."'
' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -93,6 +97,14 @@ class Bru {
delete this.runtimeVariables[key];
}
getCollectionVar(key) {
return this._interpolate(this.collectionVariables[key]);
}
getFolderVar(key) {
return this._interpolate(this.folderVariables[key]);
}
getRequestVar(key) {
return this._interpolate(this.requestVariables[key]);
}

View File

@@ -1,11 +1,34 @@
class BrunoRequest {
/**
* The following properties are available as shorthand:
* - req.url
* - req.method
* - req.headers
* - req.timeout
* - req.body
*
* Above shorthands are useful for accessing the request properties directly in the scripts
* It must be noted that the user cannot set these properties directly.
* They should use the respective setter methods to set these properties.
*/
constructor(req) {
this.req = req;
this.url = req.url;
this.method = req.method;
this.headers = req.headers;
this.body = req.data;
this.timeout = req.timeout;
/**
* We automatically parse the JSON body if the content type is JSON
* This is to make it easier for the user to access the body directly
*
* It must be noted that the request data is always a string and is what gets sent over the network
* If the user wants to access the raw data, they can use getBody({raw: true}) method
*/
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
this.body = this.__safeParseJSON(req.data);
}
}
getUrl() {
@@ -13,6 +36,7 @@ class BrunoRequest {
}
setUrl(url) {
this.url = url;
this.req.url = url;
}
@@ -37,6 +61,7 @@ class BrunoRequest {
}
setMethod(method) {
this.method = method;
this.req.method = method;
}
@@ -45,6 +70,7 @@ class BrunoRequest {
}
setHeaders(headers) {
this.headers = headers;
this.req.headers = headers;
}
@@ -53,15 +79,60 @@ class BrunoRequest {
}
setHeader(name, value) {
this.headers[name] = value;
this.req.headers[name] = value;
}
getBody() {
hasJSONContentType(headers) {
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
return contentType.includes('json');
}
/**
* Get the body of the request
*
* We automatically parse and return the JSON body if the content type is JSON
* If the user wants the raw body, they can pass the raw option as true
*/
getBody(options = {}) {
if (options.raw) {
return this.req.data;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
return this.__safeParseJSON(this.req.data);
}
return this.req.data;
}
setBody(data) {
/**
* If the content type is JSON and if the data is an object
* - We set the body property as the object itself
* - We set the request data as the stringified JSON as it is what gets sent over the network
* Otherwise
* - We set the request data as the data itself
* - We set the body property as the data itself
*
* If the user wants to override this behavior, they can pass the raw option as true
*/
setBody(data, options = {}) {
if (options.raw) {
this.req.data = data;
this.body = data;
return;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson && this.__isObject(data)) {
this.body = data;
this.req.data = this.__safeStringifyJSON(data);
return;
}
this.req.data = data;
this.body = data;
}
setMaxRedirects(maxRedirects) {
@@ -73,8 +144,34 @@ class BrunoRequest {
}
setTimeout(timeout) {
this.timeout = timeout;
this.req.timeout = timeout;
}
__safeParseJSON(str) {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
}
__safeStringifyJSON(obj) {
try {
return JSON.stringify(obj);
} catch (e) {
return obj;
}
}
__isObject(obj) {
return obj !== null && typeof obj === 'object';
}
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;
}
}
module.exports = BrunoRequest;

View File

@@ -2,14 +2,16 @@ const { interpolate } = require('@usebruno/common');
const interpolateString = (
str,
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} }
) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const combinedVars = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
process: {

View File

@@ -192,6 +192,8 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
}
const interpolationContext = {
collectionVariables: context.bru.collectionVariables,
folderVariables: context.bru.folderVariables,
requestVariables: context.bru.requestVariables,
runtimeVariables: context.bru.runtimeVariables,
envVariables: context.bru.envVariables,
@@ -238,13 +240,23 @@ class AssertRuntime {
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
if (!enabledAssertions.length) {
return [];
}
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
const bru = new Bru(
envVariables,
runtimeVariables,
processEnvVars,
undefined,
collectionVariables,
folderVariables,
requestVariables
);
const req = new BrunoRequest(request);
const res = createResponseParser(response);
@@ -255,7 +267,9 @@ class AssertRuntime {
};
const context = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
...processEnvVars,

View File

@@ -47,8 +47,10 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
@@ -162,8 +164,10 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);

View File

@@ -48,8 +48,10 @@ class TestRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);

View File

@@ -1,22 +1,10 @@
const _ = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { evaluateJsExpression, createResponseParser } = require('../utils');
const { executeQuickJsVm } = require('../sandbox/quickjs');
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
@@ -35,35 +23,6 @@ class VarsRuntime {
this.mode = props?.mode || 'developer';
}
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
if (!request?.requestVariables) {
request.requestVariables = {};
}
const enabledVars = _.filter(vars, (v) => v.enabled);
if (!enabledVars.length) {
return;
}
const bru = new Bru(envVariables, runtimeVariables, processEnvVars);
const req = new BrunoRequest(request);
const bruContext = {
bru,
req
};
const context = {
...envVariables,
...runtimeVariables,
...bruContext
};
_.each(enabledVars, (v) => {
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
request?.requestVariables && (request.requestVariables[v.name] = value);
});
}
runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledVars = _.filter(vars, (v) => v.enabled);

View File

@@ -69,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
getRequestVar.dispose();
let getFolderVar = vm.newFunction('getFolderVar', function (key) {
return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getFolderVar', getFolderVar);
getFolderVar.dispose();
let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
getCollectionVar.dispose();
const sleep = vm.newFunction('sleep', (timer) => {
const t = vm.getString(timer);
const promise = vm.newPromise();

View File

@@ -1,5 +1,6 @@
headers {
check: again
token: {{collection_pre_var_token}}
}
auth {
@@ -10,6 +11,11 @@ auth:bearer {
token: {{bearer_auth_token}}
}
vars:pre-request {
collection_pre_var: collection_pre_var_value
collection_pre_var_token: {{request_pre_var_token}}
}
docs {
# bruno-testbench 🐶

View File

@@ -0,0 +1,42 @@
meta {
name: echo bigint
type: http
seq: 6
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
headers {
foo: bar
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": 990531470713421825,
"decimal": 1.0,
"decimal2": 1.00,
"decimal3": 1.00200,
"decimal4": 0.00
}
}
assert {
res.status: eq 200
}
tests {
// todo: add tests once lossless json echo server is ready
}

View File

@@ -25,16 +25,6 @@ body:json {
}
}
vars:pre-request {
boolean: false
undefined: undefined
null: null
string: foo
number_1: 1
number_2: 0
number_3: -1
}
assert {
req.body.boolean: isBoolean false
req.body.number_1: isNumber 1
@@ -51,35 +41,4 @@ assert {
req.body.number_3: eq -1
req.body.number_2: isNumber
req.body.number_3: isNumber
boolean: eq false
undefined: eq undefined
null: eq null
string: eq foo
number_1: eq 1
number_2: eq 0
number_3: eq -1
}
tests {
test("boolean pre var", function() {
expect(bru.getRequestVar('boolean')).to.eql(false);
});
test("number pre var", function() {
expect(bru.getRequestVar('number_1')).to.eql(1);
expect(bru.getRequestVar('number_2')).to.eql(0);
expect(bru.getRequestVar('number_3')).to.eql(-1);
});
test("null pre var", function() {
expect(bru.getRequestVar('null')).to.eql(null);
});
test("undefined pre var", function() {
expect(bru.getRequestVar('undefined')).to.eql(undefined);
});
test("string pre var", function() {
expect(bru.getRequestVar('string')).to.eql('foo');
});
}

View File

@@ -0,0 +1,8 @@
meta {
name: string interpolation
}
vars:pre-request {
folder_pre_var: folder_pre_var_value
folder_pre_var_2: {{env.var1}}
}

View File

@@ -0,0 +1,48 @@
meta {
name: pre-vars
type: http
seq: 5
}
get {
url: {{host}}/ping
body: none
auth: none
}
headers {
request_pre_var: {{request_pre_var}}
}
vars:pre-request {
request_pre_var: {{folder_pre_var}}
request_pre_var_token: request_pre_var_token_value
request_pre_var_1: request_pre_var_1_value
request_pre_var_2: {{request_pre_var_1}}
}
assert {
collection_pre_var: eq collection_pre_var_value
folder_pre_var: eq folder_pre_var_value
}
tests {
test("collection pre var bru function", function() {
expect(bru.getCollectionVar('collection_pre_var')).to.eql('collection_pre_var_value');
});
test("folder pre var bru function", function() {
expect(bru.getFolderVar('folder_pre_var')).to.eql('folder_pre_var_value');
});
test("request pre var bru function", function() {
expect(bru.getRequestVar('request_pre_var')).to.eql('folder_pre_var_value');
});
test("request pre var self-interpoaltion", function() {
expect(bru.getRequestVar('request_pre_var_2')).to.eql('request_pre_var_1_value');
});
}

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)