Merge branch 'main' into bugfix/bug-884

This commit is contained in:
Andreas Schönefeldt
2023-11-19 21:45:11 +01:00
55 changed files with 2710 additions and 133 deletions

View File

@@ -26,6 +26,7 @@
"codemirror-graphql": "^1.2.5",
"cookie": "^0.6.0",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
@@ -38,6 +39,8 @@
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"jsonlint": "^1.6.3",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
@@ -45,6 +48,7 @@
"nanoid": "3.3.4",
"next": "12.3.3",
"path": "^0.12.7",
"pdfjs-dist": "^3.11.174",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
@@ -56,10 +60,12 @@
"react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0",
"react-inspector": "^6.0.2",
"react-pdf": "^7.5.1",
"react-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"styled-components": "^5.3.3",
"system": "^2.0.1",
"tailwindcss": "^2.2.19",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",

View File

@@ -10,12 +10,85 @@ import isEqual from 'lodash/isEqual';
import { getEnvironmentVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import jsonlint from 'jsonlint';
import { JSHINT } from 'jshint';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
window.JSHINT = JSHINT;
//This should be done dynamically if possible
const hintWords = [
'res',
'res.status',
'res.statusText',
'res.headers',
'res.body',
'res.getStatus()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
'req',
'req.url',
'req.method',
'req.headers',
'req.body',
'req.timeout',
'req.getUrl()',
'req.setUrl(url)',
'req.getMethod()',
'req.setMethod(method)',
'req.getHeader(name)',
'req.getHeaders()',
'req.setHeader(name, value)',
'req.setHeaders(data)',
'req.getBody()',
'req.setBody(data)',
'req.setMaxRedirects(maxRedirects)',
'req.getTimeout()',
'req.setTimeout(timeout)',
'bru',
'bru.cwd()',
'bru.getEnvName(key)',
'bru.getProcessEnv(key)',
'bru.getEnvVar(key)',
'bru.setEnvVar(key,value)',
'bru.getVar(key)',
'bru.setVar(key,value)'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
const currentLine = editor.getLine(cursor.line);
let startBru = cursor.ch;
let endBru = startBru;
while (endBru < currentLine.length && /[\w.]/.test(currentLine.charAt(endBru))) ++endBru;
while (startBru && /[\w.]/.test(currentLine.charAt(startBru - 1))) --startBru;
let curWordBru = startBru != endBru && currentLine.slice(startBru, endBru);
let start = cursor.ch;
let end = start;
while (end < currentLine.length && /[\w]/.test(currentLine.charAt(end))) ++end;
while (start && /[\w]/.test(currentLine.charAt(start - 1))) --start;
const jsHinter = CodeMirror.hint.javascript;
let result = jsHinter(editor) || { list: [] };
result.to = CodeMirror.Pos(cursor.line, end);
result.from = CodeMirror.Pos(cursor.line, start);
if (curWordBru) {
hintWords.forEach((h) => {
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h);
}
});
result.list?.sort();
}
return result;
});
CodeMirror.commands.autocomplete = (cm, hint, options) => {
cm.showHint({ hint, ...options });
};
}
export default class CodeEditor extends React.Component {
@@ -41,7 +114,8 @@ export default class CodeEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
lint: { esversion: 11 },
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
@@ -68,9 +142,16 @@ export default class CodeEditor extends React.Component {
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
},
'Shift-Tab': 'indentLess',
'Ctrl-Space': 'autocomplete',
'Cmd-Space': 'autocomplete',
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
@@ -103,9 +184,30 @@ export default class CodeEditor extends React.Component {
}
}));
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
editor.on('change', this._onEdit);
this.addOverlay();
}
if (this.props.mode == 'javascript') {
editor.on('keyup', function (cm, event) {
const cursor = editor.getCursor();
const currentLine = editor.getLine(cursor.line);
let start = cursor.ch;
let end = start;
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
let curWord = start != end && currentLine.slice(start, end);
//Qualify if autocomplete will be shown
if (
/^(?!Shift|Tab|Enter|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|\s)\w*/.test(event.key) &&
curWord.length > 0 &&
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
) {
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.brunoJS, { completeSingle: false });
}
});
}
}
componentDidUpdate(prevProps) {
@@ -172,6 +274,7 @@ export default class CodeEditor extends React.Component {
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
this.cachedValue = this.editor.getValue();
if (this.props.onEdit) {
this.props.onEdit(this.cachedValue);

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
table {
td {
&:first-child {
width: 120px;
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
function countRequests(items) {
let count = 0;
@@ -20,9 +20,9 @@ function countRequests(items) {
return count;
}
const CollectionProperties = ({ collection, onClose }) => {
const Info = ({ collection }) => {
return (
<Modal size="sm" title="Collection Properties" hideFooter={true} handleCancel={onClose}>
<StyledWrapper className="w-full flex flex-col h-full">
<table className="w-full border-collapse">
<tbody>
<tr className="">
@@ -43,8 +43,8 @@ const CollectionProperties = ({ collection, onClose }) => {
</tr>
</tbody>
</table>
</Modal>
</StyledWrapper>
);
};
export default CollectionProperties;
export default Info;

View File

@@ -0,0 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 110px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,96 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import cloneDeep from 'lodash/cloneDeep';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
brunoConfig: { presets: presets = {} }
} = collection;
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestType: presets.requestType || 'http',
requestUrl: presets.requestUrl || ''
},
onSubmit: (newPresets) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.presets = newPresets;
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
toast.success('Collection presets updated');
}
});
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Collection Presets</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Request Type
</label>
<div className="flex items-center">
<input
id="http"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="http"
checked={formik.values.requestType === 'http'}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
</label>
<input
id="graphql"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="graphql"
checked={formik.values.requestType === 'graphql'}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="requestUrl">
Base URL
</label>
<div className="flex items-center">
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
/>
</div>
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default PresetsSettings;

View File

@@ -11,22 +11,20 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: true,
is: 'true',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.when('enabled', {
is: true,
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
})
.min(1)
.max(65535),
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: true,
is: 'true',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()

View File

@@ -13,6 +13,8 @@ import Auth from './Auth';
import Script from './Script';
import Test from './Tests';
import Docs from './Docs';
import Presets from './Presets';
import Info from './Info';
import StyledWrapper from './StyledWrapper';
const CollectionSettings = ({ collection }) => {
@@ -84,6 +86,9 @@ const CollectionSettings = ({ collection }) => {
case 'tests': {
return <Test collection={collection} />;
}
case 'presets': {
return <Presets collection={collection} />;
}
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
}
@@ -99,6 +104,9 @@ const CollectionSettings = ({ collection }) => {
case 'docs': {
return <Docs collection={collection} />;
}
case 'info': {
return <Info collection={collection} />;
}
}
};
@@ -123,6 +131,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
Tests
</div>
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
Presets
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
</div>
@@ -132,6 +143,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
<div className={getTabClassname('info')} role="tab" onClick={() => setTab('info')}>
Info
</div>
</div>
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
{getTabPanel(tab)}

View File

@@ -32,7 +32,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
secret: Yup.boolean(),
type: Yup.string(),
uid: Yup.string(),
value: Yup.string().trim()
value: Yup.string().trim().nullable()
})
),
onSubmit: (values) => {
@@ -114,7 +114,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
className="mousetrap"
id={`${index}.name`}
name={`${index}.name`}
value={formik.values[index].name}
value={variable.name}
onChange={formik.handleChange}
/>
<ErrorMessage name={`${index}.name`} />

View File

@@ -22,13 +22,11 @@ const ProxySettings = ({ close }) => {
})
.max(1024),
port: Yup.number()
.when('enabled', {
is: true,
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
})
.min(1)
.max(65535),
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: true,

View File

@@ -34,7 +34,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
url: value && typeof value === 'string' ? value.trim() : value
})
);
};

View File

@@ -42,7 +42,7 @@ const CollectionToolBar = ({ collection }) => {
return (
<StyledWrapper>
<div className="flex items-center p-2">
<div className="flex flex-1 items-center">
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
<IconFiles size={18} strokeWidth={1.5} />
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>

View File

@@ -2,6 +2,11 @@ import CodeEditor from 'components/CodeEditor/index';
import { get } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { Document, Page } from 'react-pdf';
import { useState } from 'react';
import 'pdfjs-dist/build/pdf.worker';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
const QueryResultPreview = ({
previewTab,
@@ -19,6 +24,10 @@ const QueryResultPreview = ({
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
const [numPages, setNumPages] = useState(null);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
// Fail safe, so we don't render anything with an invalid tab
if (!allowedPreviewModes.includes(previewTab)) {
return null;
@@ -45,6 +54,17 @@ const QueryResultPreview = ({
case 'preview-image': {
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
}
case 'preview-pdf': {
return (
<div className="preview-pdf" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>
<Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>
{Array.from(new Array(numPages), (el, index) => (
<Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />
))}
</Document>
</div>
);
}
default:
case 'raw': {
return (

View File

@@ -18,6 +18,19 @@ const StyledWrapper = styled.div`
width: 100%;
}
.react-pdf__Page {
margin-top: 10px;
background-color: transparent !important;
}
.react-pdf__Page__textContent {
border: 1px solid darkgrey;
box-shadow: 5px 5px 5px 1px #ccc;
border-radius: 0px;
margin: 0 auto;
}
.react-pdf__Page__canvas {
margin: 0 auto;
}
div[role='tablist'] {
.active {
color: ${(props) => props.theme.colors.text.yellow};

View File

@@ -49,6 +49,8 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
allowedPreviewModes.unshift('preview-web');
} else if (mode.includes('image')) {
allowedPreviewModes.unshift('preview-image');
} else if (contentType.includes('pdf')) {
allowedPreviewModes.unshift('preview-pdf');
}
return allowedPreviewModes;

View File

@@ -9,10 +9,11 @@ const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const { target, client, language: lang } = language;
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
let snippet = '';
try {
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
} catch (e) {
console.error(e);
snippet = 'Error generating code snippet';

View File

@@ -2,7 +2,6 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
@@ -15,7 +14,6 @@ import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import ExportCollection from './ExportCollection';
import CollectionProperties from './CollectionProperties';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
import exportCollection from 'utils/collections/export';
@@ -29,7 +27,6 @@ const Collection = ({ collection, searchText }) => {
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false);
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
const dispatch = useDispatch();
@@ -80,9 +77,14 @@ const Collection = ({ collection, searchText }) => {
}
};
const handleExportClick = () => {
const collectionCopy = cloneDeep(collection);
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
const [{ isOver }, drop] = useDrop({
@@ -131,9 +133,6 @@ const Collection = ({ collection, searchText }) => {
{showExportCollectionModal && (
<ExportCollection collection={collection} onClose={() => setShowExportCollectionModal(false)} />
)}
{collectionPropertiesModal && (
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
)}
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
@@ -201,19 +200,19 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setCollectionPropertiesModal(true);
setShowRemoveCollectionModal(true);
}}
>
Properties
Remove
</div>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowRemoveCollectionModal(true);
viewCollectionSettings();
}}
>
Remove
Settings
</div>
</Dropdown>
</div>

View File

@@ -35,6 +35,10 @@ const StyledWrapper = styled.div`
}
}
}
textarea.curl-command {
min-height: 150px;
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useCallback } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
@@ -16,12 +16,36 @@ import { getRequestFromCurlCommand } from 'utils/curl';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const {
brunoConfig: { presets: collectionPresets = {} }
} = collection;
const getRequestType = (collectionPresets) => {
if (!collectionPresets || !collectionPresets.requestType) {
return 'http-request';
}
// Note: Why different labels for the same thing?
// http-request and graphql-request are used inside the app's json representation of a request
// http and graphql are used in Bru DSL as well as collection exports
// We need to eventually standardize the app's DSL to use the same labels as bru DSL
if (collectionPresets.requestType === 'http') {
return 'http-request';
}
if (collectionPresets.requestType === 'graphql') {
return 'graphql-request';
}
return 'http-request';
};
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestName: '',
requestType: 'http-request',
requestUrl: '',
requestType: getRequestType(collectionPresets),
requestUrl: collectionPresets.requestUrl || '',
requestMethod: 'GET',
curlCommand: ''
},
@@ -115,6 +139,25 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const onSubmit = () => formik.handleSubmit();
const handlePaste = useCallback(
(event) => {
const clipboardData = event.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData('Text');
// Check if pasted data looks like a cURL command
const curlCommandRegex = /^\s*curl\s/i;
if (curlCommandRegex.test(pastedData)) {
// Switch to the 'from-curl' request type
formik.setFieldValue('requestType', 'from-curl');
formik.setFieldValue('curlCommand', pastedData);
// Prevent the default paste behavior to avoid pasting into the textarea
event.preventDefault();
}
},
[formik]
);
return (
<StyledWrapper>
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
@@ -174,7 +217,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
Name
</label>
<input
id="collection-name"
id="request-name"
type="text"
name="requestName"
ref={inputRef}
@@ -216,6 +259,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onPaste={handlePaste}
/>
</div>
</div>
@@ -232,8 +276,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4"
style={{ resize: 'none' }}
className="block textbox w-full mt-4 curl-command"
value={formik.values.curlCommand}
onChange={formik.handleChange}
></textarea>

View File

@@ -105,7 +105,7 @@ const Sidebar = () => {
Star
</GitHubButton>
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.1</div>
</div>
</div>
</div>

View File

@@ -22,8 +22,11 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/fold/brace-fold');
require('codemirror/addon/fold/foldgutter');
require('codemirror/addon/fold/xml-fold');
require('codemirror/addon/hint/javascript-hint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/lint/lint');
require('codemirror/addon/lint/javascript-lint');
require('codemirror/addon/lint/json-lint');
require('codemirror/addon/mode/overlay');
require('codemirror/addon/scroll/simplescrollbars');
require('codemirror/addon/search/jump-to-line');

View File

@@ -9,25 +9,17 @@ const createContentType = (mode) => {
case 'multipartForm':
return 'multipart/form-data';
default:
return 'application/json';
return '';
}
};
const createHeaders = (headers, mode) => {
const contentType = createContentType(mode);
const headersArray = headers
const createHeaders = (headers) => {
return headers
.filter((header) => header.enabled)
.map((header) => {
return {
name: header.name,
value: header.value
};
});
const headerNames = headersArray.map((header) => header.name);
if (!headerNames.includes('Content-Type')) {
return [...headersArray, { name: 'Content-Type', value: contentType }];
}
return headersArray;
.map((header) => ({
name: header.name,
value: header.value
}));
};
const createQuery = (queryParams = []) => {
@@ -56,13 +48,13 @@ const createPostData = (body) => {
}
};
export const buildHarRequest = (request) => {
export const buildHarRequest = ({ request, headers }) => {
return {
method: request.method,
url: request.url,
httpVersion: 'HTTP/1.1',
cookies: [],
headers: createHeaders(request.headers, request.body.mode),
headers: createHeaders(headers),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,

View File

@@ -46,8 +46,8 @@ describe('curlToJson', () => {
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: '%27https://www.usebruno.com%27',
raw_url: "'https://www.usebruno.com'",
url: 'https://www.usebruno.com',
raw_url: 'https://www.usebruno.com',
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',

View File

@@ -187,18 +187,24 @@ const transformOpenapiRequestItem = (request) => {
return brunoRequestItem;
};
const resolveRefs = (spec, components = spec.components) => {
const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
if (Array.isArray(spec)) {
return spec.map((item) => resolveRefs(item, components));
return spec.map((item) => resolveRefs(item, components, visitedItems));
}
if ('$ref' in spec) {
const refPath = spec.$ref;
if (visitedItems.has(refPath)) {
return spec;
} else {
visitedItems.add(refPath);
}
if (refPath.startsWith('#/components/')) {
// Local reference within components
const refKeys = refPath.replace('#/components/', '').split('/');
@@ -213,7 +219,7 @@ const resolveRefs = (spec, components = spec.components) => {
}
}
return resolveRefs(ref, components);
return resolveRefs(ref, components, visitedItems);
} else {
// Handle external references (not implemented here)
// You would need to fetch the external reference and resolve it.
@@ -223,7 +229,7 @@ const resolveRefs = (spec, components = spec.components) => {
// Recursively resolve references in nested objects
for (const prop in spec) {
spec[prop] = resolveRefs(spec[prop], components);
spec[prop] = resolveRefs(spec[prop], components, visitedItems);
}
return spec;
@@ -267,12 +273,7 @@ const getDefaultUrl = (serverObject) => {
};
const getSecurity = (apiSpec) => {
let supportedSchemes = apiSpec.security || [];
if (supportedSchemes.length === 0) {
return {
supported: []
};
}
let defaultSchemes = apiSpec.security || [];
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
if (Object.keys(securitySchemes) === 0) {
@@ -282,7 +283,7 @@ const getSecurity = (apiSpec) => {
}
return {
supported: supportedSchemes.map((scheme) => {
supported: defaultSchemes.map((scheme) => {
var schemeName = Object.keys(scheme)[0];
return securitySchemes[schemeName];
}),

View File

@@ -1,5 +1,9 @@
# Changelog
## 1.1.0
- Upgraded axios to 1.5.1
## 1.0.0
- Announcing Stable Release

View File

@@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
"version": "1.0.0",
"version": "1.1.1",
"license": "MIT",
"main": "src/index.js",
"bin": {
@@ -24,7 +24,7 @@
"package.json"
],
"dependencies": {
"@usebruno/js": "0.9.1",
"@usebruno/js": "0.9.2",
"@usebruno/lang": "0.9.0",
"axios": "^1.5.1",
"chai": "^4.3.7",

View File

@@ -3,7 +3,7 @@ const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
const { forOwn, each, extend, get, compact } = require('lodash');
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
const FormData = require('form-data');
const prepareRequest = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
@@ -136,14 +136,15 @@ const runSingleRequest = async function (
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {

View File

@@ -1,5 +1,5 @@
{
"version": "v1.1.0",
"version": "v1.1.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -20,7 +20,7 @@
},
"dependencies": {
"@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.9.1",
"@usebruno/js": "0.9.2",
"@usebruno/lang": "0.9.0",
"@usebruno/schema": "0.6.0",
"about-window": "^1.15.2",

View File

@@ -6,11 +6,10 @@ const axios = require('axios');
const path = require('path');
const decomment = require('decomment');
const Mustache = require('mustache');
const FormData = require('form-data');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
const { forOwn, extend, each, get, compact } = require('lodash');
const { isUndefined, isNull, each, get, compact } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
@@ -72,22 +71,6 @@ const getEnvVars = (environment = {}) => {
};
};
const getSize = (data) => {
if (!data) {
return 0;
}
if (typeof data === 'string') {
return Buffer.byteLength(data, 'utf8');
}
if (typeof data === 'object') {
return Buffer.byteLength(safeStringifyJSON(data), 'utf8');
}
return 0;
};
const configureRequest = async (
collectionUid,
request,
@@ -143,7 +126,7 @@ const configureRequest = async (
// proxy configuration
let proxyConfig = get(brunoConfig, 'proxy', {});
let proxyEnabled = get(proxyConfig, 'enabled', false);
let proxyEnabled = get(proxyConfig, 'enabled', 'global');
if (proxyEnabled === 'global') {
proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyEnabled = get(proxyConfig, 'enabled', false);
@@ -156,14 +139,15 @@ const configureRequest = async (
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
@@ -203,10 +187,10 @@ const configureRequest = async (
const parseDataFromResponse = (response) => {
const dataBuffer = Buffer.from(response.data);
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charset = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
const charset = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
// Overwrite the original data for backwards compatability
let data = dataBuffer.toString(charset || 'utf-8');
// Try to parse response to JSON, this can quitly fail
// Try to parse response to JSON, this can quietly fail
try {
data = JSON.parse(response.data);
} catch {}

View File

@@ -1,6 +1,6 @@
{
"name": "@usebruno/js",
"version": "0.9.1",
"version": "0.9.2",
"license": "MIT",
"main": "src/index.js",
"files": [

View File

@@ -45,7 +45,7 @@ class Bru {
setEnvVar(key, value) {
if (!key) {
throw new Error('Key is required');
throw new Error('Creating a env variable without specifying a name is not allowed.');
}
// gracefully ignore if key is not present in environment
@@ -58,7 +58,7 @@ class Bru {
setVar(key, value) {
if (!key) {
throw new Error('Key is required');
throw new Error('Creating a variable without specifying a name is not allowed.');
}
if (envVariableNameRegex.test(key) === false) {