Merge branch 'usebruno:main' into feat/digest-auth-updates

This commit is contained in:
Pragadesh-45
2025-01-02 17:37:33 +05:30
committed by GitHub
25 changed files with 572 additions and 380 deletions

View File

@@ -2,6 +2,11 @@ name: Bru CLI Tests (npm)
on:
workflow_dispatch:
inputs:
build:
description: 'Test Bru CLI (npm)'
required: true
default: 'true'
# Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168

730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,20 +35,20 @@
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.6",
"i18next": "^23.14.0",
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "10.1.0",
"jsonpath-plus": "10.2.0",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"nanoid": "3.3.8",
"path": "^0.12.7",
"pdfjs-dist": "4.4.168",
"platform": "^1.3.6",

View File

@@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
{activeEnvironment ? activeEnvironment.name : 'No Environment'}
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<IconCaretDown className="caret" size={14} strokeWidth={2} />
</div>
);

View File

@@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
text-decoration: none;

View File

@@ -8,6 +8,7 @@ import ImportEnvironment from '../ImportEnvironment';
import ManageSecrets from '../ManageSecrets';
import StyledWrapper from './StyledWrapper';
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ToolHint from 'components/ToolHint';
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => {
const { environments } = collection;
@@ -103,13 +104,15 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
{environments &&
environments.length &&
environments.map((env) => (
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
>
<span className="break-all">{env.name}</span>
</div>
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
<div
id={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
>
<span className="break-all">{env.name}</span>
</div>
</ToolHint>
))}
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
+ <span>Create</span>

View File

@@ -90,7 +90,7 @@ const parseAssertionOperator = (str = '') => {
'isArray'
];
const [operator, ...rest] = str.trim().split(' ');
const [operator, ...rest] = str.split(' ');
const value = rest.join(' ');
if (unaryOperators.includes(operator)) {
@@ -166,7 +166,7 @@ const AssertionRow = ({
handleAssertionChange(
{
target: {
value: `${op} ${value}`
value: isUnaryOperator(op) ? op : `${op} ${value}`
}
},
assertion,
@@ -182,7 +182,7 @@ const AssertionRow = ({
theme={storedTheme}
readOnly={true}
onSave={onSave}
onChange={(newValue) =>
onChange={(newValue) => {
handleAssertionChange(
{
target: {
@@ -192,6 +192,7 @@ const AssertionRow = ({
assertion,
'value'
)
}
}
onRun={handleRun}
collection={collection}

View File

@@ -117,7 +117,6 @@ const QueryParams = ({ item, collection }) => {
<StyledWrapper className="w-full flex flex-col absolute">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div>
<Table
headers={[
{ name: 'Name', accessor: 'name', width: '31%' },
@@ -153,7 +152,7 @@ const QueryParams = ({ item, collection }) => {
/>
</td>
<td>
<div className="flex items-center">
<div className="flex items-center justify-center">
<input
type="checkbox"
checked={param.enabled}
@@ -188,7 +187,7 @@ const QueryParams = ({ item, collection }) => {
</code>
</div>
`}
infotipId="path-param-InfoTip"
infotipId="path-param-InfoTip"
/>
</div>
<table>
@@ -241,11 +240,7 @@ const QueryParams = ({ item, collection }) => {
: null}
</tbody>
</table>
{!(pathParams && pathParams.length) ?
<div className="title pr-2 py-3 mt-2 text-xs">
</div>
: null}
{!(pathParams && pathParams.length) ? <div className="title pr-2 py-3 mt-2 text-xs"></div> : null}
</div>
</StyledWrapper>
);

View File

@@ -39,12 +39,15 @@ const RequestTabPanel = () => {
const _collections = useSelector((state) => state.collections.collections);
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
let collections = produce(_collections, draft => {
let collections = produce(_collections, (draft) => {
let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);
if (collection) {
// add selected global env variables to the collection object
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
globalEnvironments,
activeGlobalEnvironmentUid
});
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
collection.globalEnvironmentVariables = globalEnvironmentVariables;
collection.globalEnvSecrets = globalEnvSecrets;

View File

@@ -20,14 +20,14 @@ const formatResponse = (data, mode, filter) => {
}
if (data === null) {
return data;
return 'null';
}
if (mode.includes('json')) {
let isValidJSON = false;
try {
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object';
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
} catch (error) {
console.log('Error parsing JSON: ', error.message);
}

View File

@@ -35,6 +35,28 @@ const StyledWrapper = styled.div`
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
}
}
.flexible-container {
width: 100%;
}
@media (max-width: 600px) {
.flexible-container {
width: 500px;
}
}
@media (min-width: 601px) and (max-width: 1200px) {
.flexible-container {
width: 800px;
}
}
@media (min-width: 1201px) {
.flexible-container {
width: 900px;
}
}
`;
export default StyledWrapper;

View File

@@ -48,7 +48,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
return (
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
<StyledWrapper>
<div className="flex w-full">
<div className="flex w-full flexible-container">
<div>
<div className="generate-code-sidebar">
{languages &&
@@ -59,7 +59,27 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
className={
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
}
role="button"
tabIndex={0}
onClick={() => setSelectedLanguage(language)}
onKeyDown={(e) => {
if (e.key === 'Tab') {
e.preventDefault();
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
const nextIndex = e.shiftKey
? (currentIndex - 1 + languages.length) % languages.length
: (currentIndex + 1) % languages.length;
setSelectedLanguage(languages[nextIndex]);
}
if (e.shiftKey && e.key === 'Tab') {
e.preventDefault();
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
const nextIndex = (currentIndex - 1 + languages.length) % languages.length;
setSelectedLanguage(languages[nextIndex]);
}
}}
aria-pressed={language.name === selectedLanguage.name}
>
<span className="capitalize">{language.name}</span>
</div>

View File

@@ -128,13 +128,29 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings'
})
);
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleFolderCollapse = () => {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
}
const handleRightClick = (event) => {
const _menuDropdown = dropdownTippyRef.current;
@@ -260,9 +276,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
})
: null}
<div
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
className="flex flex-grow items-center h-full overflow-hidden"
style={{
paddingLeft: 8
@@ -275,11 +288,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
strokeWidth={2}
className={iconClassName}
style={{ color: 'rgb(160 160 160)' }}
onClick={handleFolderCollapse}
/>
) : null}
</div>
<div className="ml-1 flex items-center overflow-hidden">
<div
className="ml-1 flex items-center overflow-hidden flex-1"
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
>
<RequestMethod item={item} />
<span className="item-name" title={item.name}>
{item.name}

View File

@@ -68,6 +68,17 @@ const Collection = ({ collection, searchText }) => {
dispatch(collectionClicked(collection.uid));
};
const handleCollapseCollection = () => {
dispatch(collectionClicked(collection.uid));
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
}
const handleRightClick = (event) => {
const _menuDropdown = menuDropdownTippyRef.current;
if (_menuDropdown) {
@@ -141,16 +152,17 @@ const Collection = ({ collection, searchText }) => {
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
onClick={handleClick}
onContextMenu={handleRightClick}
>
<IconChevronRight
size={16}
strokeWidth={2}
className={iconClassName}
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
onClick={handleClick}
/>
<div className="ml-1" id="sidebar-collection-name">
<div className="ml-1" id="sidebar-collection-name"
onClick={handleCollapseCollection}
onContextMenu={handleRightClick}>
{collection.name}
</div>
</div>

View File

@@ -57,7 +57,7 @@ function getDataString(request) {
console.error('Failed to parse JSON data:', error);
return { data: request.data.toString() };
}
} else if (contentType && contentType.includes('application/xml')) {
} else if (contentType && (contentType.includes('application/xml') || contentType.includes('text/plain'))) {
return { data: request.data };
}
@@ -174,14 +174,14 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
if(request.auth.mode === 'basic'){
if (request.auth.mode === 'basic') {
requestJson.auth = {
mode: 'basic',
basic: {
username: repr(request.auth.basic?.username),
password: repr(request.auth.basic?.password)
}
}
};
}
}

View File

@@ -291,8 +291,9 @@ export const exportCollection = (collection) => {
};
}
default: {
console.error('Unsupported auth mode:', itemAuth.mode);
return null;
return {
type: 'noauth'
};
}
}
};

View File

@@ -136,8 +136,7 @@ const prepareRequest = (item = {}, collection = {}) => {
if (request.body.mode === 'multipartForm') {
axiosRequest.headers['content-type'] = 'multipart/form-data';
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
const collectionPath = process.cwd();
axiosRequest.data = createFormData(enabledParams, collectionPath);
axiosRequest.data = enabledParams;
}
if (request.body.mode === 'graphql') {

View File

@@ -38,10 +38,14 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
if (!disableParsingResponseJson) {
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) {
data = JSON.parse(data);
}
} catch { }
} catch {
console.log('Failed to parse response data as JSON');
}
return { data, dataBuffer };
};

View File

@@ -3,7 +3,7 @@ require('dotenv').config({ path: process.env.DOTENV_PATH });
const config = {
appId: 'com.usebruno.app',
productName: 'Bruno',
electronVersion: '31.2.1',
electronVersion: '33.2.1',
directories: {
buildResources: 'resources',
output: 'out'

View File

@@ -50,7 +50,7 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "3.3.4",
"nanoid": "3.3.8",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
@@ -62,7 +62,7 @@
"dmg-license": "^1.0.11"
},
"devDependencies": {
"electron": "31.2.1",
"electron": "33.2.1",
"electron-builder": "25.1.8"
}
}

View File

@@ -366,10 +366,14 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
if (!disableParsingResponseJson) {
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) {
data = JSON.parse(data);
}
} catch { }
} catch {
console.log('Failed to parse response data as JSON');
}
return { data, dataBuffer };
};

View File

@@ -16,6 +16,7 @@
},
"dependencies": {
"@usebruno/common": "0.1.0",
"@usebruno/crypto-js": "^3.1.9",
"@usebruno/query": "0.1.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -25,11 +26,10 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
"crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.4",
"nanoid": "3.3.8",
"node-fetch": "^2.7.0",
"node-vault": "^0.10.2",
"path": "^0.12.7",

View File

@@ -11,7 +11,7 @@ const bundleLibraries = async () => {
import moment from "moment";
import btoa from "btoa";
import atob from "atob";
import * as CryptoJS from "crypto-js-3.1.9-1";
import * as CryptoJS from "@usebruno/crypto-js";
globalThis.expect = expect;
globalThis.assert = assert;
globalThis.moment = moment;

View File

@@ -11,6 +11,7 @@ post {
}
body:multipart-form {
foo: {"bar":"baz"} @contentType(application/json--test)
form-data-key: {{form-data-key}}
form-data-stringified-object: {{form-data-stringified-object}}
file: @file(bruno.png)
@@ -19,6 +20,7 @@ body:multipart-form {
assert {
res.body: contains form-data-value
res.body: contains {"foo":123}
res.body: contains Content-Type: application/json--test
}
script:pre-request {

View File

@@ -1,24 +0,0 @@
meta {
name: mixed-content-types
type: http
seq: 1
}
post {
url: {{host}}/api/multipart/mixed-content-types
body: multipartForm
auth: none
}
body:multipart-form {
param1: test
param2: {"test":"i am json"} @contentType(application/json)
param3: @file(multipart/small.png)
}
assert {
res.status: eq 200
res.body.find(p=>p.name === 'param1').contentType: isUndefined
res.body.find(p=>p.name === 'param2').contentType: eq application/json
res.body.find(p=>p.name === 'param3').contentType: eq image/png
}