Merge branch 'main' into feature/filter-by-url

This commit is contained in:
Niklas Ziermann 2024-08-14 15:10:25 +02:00 committed by GitHub
commit 9e565273da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 343 additions and 60 deletions

View File

@ -40,10 +40,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.dropdown.iconColor};
}
&:hover {
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:disabled {
cursor: not-allowed;
color: gray;
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
}

View File

@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
<div className="text-xs mb-4 text-muted">
Pre and post-request scripts that will run before and after any request inside this folder is sent.
</div>
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
@ -56,8 +56,8 @@ const Script = ({ collection, folder }) => {
font={get(preferences, 'font.codeFont', 'default')}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}

View File

@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
};
return (
<StyledWrapper>
<StyledWrapper className="flex flex-col h-full">
<div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>

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 mr-3"
className="tooltip mx-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
const RequestTab = ({ tab, collection, folderUid }) => {
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
const handleRightClick = (_event) => {
const menuDropdown = dropdownTippyRef.current;
if (!menuDropdown) {
return;
}
if (menuDropdown.state.isShown) {
menuDropdown.hide();
} else {
menuDropdown.show();
}
};
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
@ -143,6 +162,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
)}
<div
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
@ -159,6 +179,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
/>
</div>
<div
className="flex px-2 close-icon-container"
@ -195,4 +224,124 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
const totalTabs = collectionRequestTabs.length || 0;
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
const currentTabItem = findItemInCollection(collection, currentTabUid);
const hasLeftTabs = tabIndex !== 0;
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
async function handleCloseTab(event, tabUid) {
event.stopPropagation();
dropdownTippyRef.current.hide();
if (!tabUid) {
return;
}
try {
const item = findItemInCollection(collection, tabUid);
// silently save unsaved changes before closing the tab
if (item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
dispatch(closeTabs({ tabUids: [tabUid] }));
} catch (err) {}
}
function handleCloseOtherTabs(event) {
dropdownTippyRef.current.hide();
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheLeft(event) {
dropdownTippyRef.current.hide();
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheRight(event) {
dropdownTippyRef.current.hide();
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseSavedTabs(event) {
event.stopPropagation();
const savedTabs = collection.items.filter((item) => !item.draft);
const savedTabIds = savedTabs.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
function handleCloseAllTabs(event) {
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collection={collection}
onClose={() => setShowCloneRequestModal(false)}
/>
)}
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowAddNewRequestModal(true);
}}
>
New Request
</button>
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowCloneRequestModal(true);
}}
>
Clone Request
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
Close
</button>
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
Close Others
</button>
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
Close to the Left
</button>
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
Close to the Right
</button>
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
Close Saved
</button>
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
Close All
</button>
</Dropdown>
</Fragment>
);
}
export default RequestTab;

View File

@ -7,7 +7,6 @@ const Wrapper = styled.div`
padding: 0;
margin: 0;
display: flex;
position: relative;
overflow: scroll;
&::-webkit-scrollbar {

View File

@ -110,7 +110,14 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
<RequestTab
collectionRequestTabs={collectionRequestTabs}
tabIndex={index}
key={tab.uid}
tab={tab}
collection={activeCollection}
folderUid={tab.folderUid}
/>
</li>
);
})

View File

@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
body: request.body
body: request.body,
auth: request.auth
})
)
.then(() => onClose())

View File

@ -129,7 +129,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.24.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.25.0</div>
</div>
</div>
</div>

View File

@ -174,7 +174,7 @@ class SingleLineEditor extends Component {
render() {
return (
<div className="flex flex-row justify-between w-full">
<div className="flex flex-row justify-between w-full overflow-x-auto">
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
{this.secretEye(this.props.isSecret)}
</div>

View File

@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import StyledWrapper from './StyledWrapper';
import { useDictionary } from 'providers/Dictionary/index';
const Welcome = () => {
const dispatch = useDispatch();
const { dictionary } = useDictionary();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@ -20,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
(err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
);
};
@ -38,12 +40,12 @@ const Welcome = () => {
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
toast.success('Collection imported successfully');
toast.success(dictionary.collectionImportedSuccessfully);
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
toast.error('An error occurred while importing the collection. Check the logs for more information.');
toast.error(dictionary.errorWhileImportingCollection);
});
};
@ -66,46 +68,45 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
<div className="mt-4">{dictionary.aboutBruno}</div>
<div className="uppercase font-semibold heading mt-10">Collections</div>
<div className="uppercase font-semibold heading mt-10">{dictionary.collections}</div>
<div className="mt-4 flex items-center collection-options select-none">
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
<IconPlus size={18} strokeWidth={2} />
<span className="label ml-2" id="create-collection">
Create Collection
{dictionary.createCollection}
</span>
</div>
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
<IconFolders size={18} strokeWidth={2} />
<span className="label ml-2">Open Collection</span>
<span className="label ml-2">{dictionary.openCollection}</span>
</div>
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
<IconDownload size={18} strokeWidth={2} />
<span className="label ml-2" id="import-collection">
Import Collection
{dictionary.importCollection}
</span>
</div>
</div>
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
<div className="mt-4 flex flex-col collection-options select-none">
<div className="flex items-center mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span>
<span className="label ml-2">{dictionary.documentation}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
<span className="label ml-2">{dictionary.reportIssues}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span>
<span className="label ml-2">{dictionary.gitHub}</span>
</a>
</div>
</div>

View File

@ -0,0 +1,14 @@
export default {
aboutBruno: 'Opensource IDE for exploring and testing APIs',
collections: 'Collections',
createCollection: 'Create Collection',
openCollection: 'Open Collection',
importCollection: 'Import Collection',
documentation: 'Documentation',
reportIssues: 'Report Issues',
gitHub: 'GitHub',
collectionImportedSuccessfully: 'Collection imported successfully',
errorWhileOpeningCollection: 'An error occurred while opening the collection',
errorWhileImportingCollection:
'An error occurred while importing the collection. Check the logs for more information.'
};

View File

@ -0,0 +1,5 @@
import en from './en.js';
export const dictionaries = {
en
};

View File

@ -14,6 +14,7 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
import { DictionaryProvider } from 'providers/Dictionary/index';
function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
@ -59,13 +60,15 @@ function MyApp({ Component, pageProps }) {
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
<DictionaryProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</DictionaryProvider>
</ThemeProvider>
</Provider>
</NoSsr>

View File

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

View File

@ -0,0 +1,28 @@
import React from 'react';
import { useState, useContext } from 'react';
import { dictionaries } from 'src/dictionaries/index';
export const DictionaryContext = React.createContext();
const DictionaryProvider = (props) => {
const [language, setLanguage] = useState('en');
const dictionary = dictionaries[language] ?? dictionaries.en;
return (
<DictionaryContext.Provider {...props} value={{ language, setLanguage, dictionary }}>
<>{props.children}</>
</DictionaryContext.Provider>
);
};
const useDictionary = () => {
const context = useContext(DictionaryContext);
if (context === undefined) {
throw new Error(`useDictionary must be used within a DictionaryProvider`);
}
return context;
};
export { useDictionary, DictionaryProvider };

View File

@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
// close all tabs
useEffect(() => {
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if (collection) {
const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
dispatch(
closeTabs({
tabUids: tabUids
})
);
}
}
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
};
}, [activeTabUid, tabs, collections, dispatch]);
return (
<HotkeysContext.Provider {...props} value="hotkey">
{showSaveRequestModal && (

View File

@ -698,7 +698,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => {
const state = getState();
@ -730,6 +730,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
sparql: null,
multipartForm: null,
formUrlEncoded: null
},
auth: auth ?? {
mode: 'none'
}
}
};

View File

@ -4,8 +4,8 @@ import find from 'lodash/find';
export const doesRequestMatchSearchText = (request, searchText = '') => {
return (
request.name.toLowerCase().includes(searchText.toLowerCase()) ||
request.request.url.toLowerCase().includes(searchText.toLowerCase())
request?.name?.toLowerCase().includes(searchText.toLowerCase()) ||
request?.request?.url?.toLowerCase().includes(searchText.toLowerCase())
);
};

View File

@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => {
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
}
requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
const splitAuth = request.auth.split(':');
const user = splitAuth[0] || '';
const password = splitAuth[1] || '';
requestJson.auth = {
user: repr(user),
password: repr(password)
};
if(request.auth.mode === 'basic'){
requestJson.auth = {
mode: 'basic',
basic: {
username: repr(request.auth.basic?.username),
password: repr(request.auth.basic?.password)
}
}
}
}
return Object.keys(requestJson).length ? requestJson : {};

View File

@ -75,4 +75,15 @@ describe('curlToJson', () => {
}
});
});
it('should return and parse a simple curl command with a trailing slash', () => {
const curlCommand = 'curl https://www.usebruno.com/';
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: 'https://www.usebruno.com/',
raw_url: 'https://www.usebruno.com/',
method: 'get'
});
});
});

View File

@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
url: request.url,
method: request.method,
body,
headers: headers
headers: headers,
auth: request.auth
};
} catch (error) {
console.error(error);

View File

@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => {
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
alias: {
H: 'header',
A: 'user-agent'
A: 'user-agent',
u: 'user'
}
});
@ -187,10 +188,21 @@ const parseCurlCommand = (curlCommand) => {
}
urlObject.search = null; // Clean out the search/query portion.
let urlWithoutQuery = URL.format(urlObject);
let urlHost = urlObject?.host;
if (!url?.includes(`${urlHost}/`)) {
if (urlWithoutQuery && urlHost) {
const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost);
urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1);
}
}
const request = {
url: url,
urlWithoutQuery: URL.format(urlObject)
url,
urlWithoutQuery
};
if (compressed) {
request.compressed = true;
}
@ -226,12 +238,19 @@ const parseCurlCommand = (curlCommand) => {
request.data = parsedArguments['data-urlencode'];
}
if (parsedArguments.u) {
request.auth = parsedArguments.u;
}
if (parsedArguments.user) {
request.auth = parsedArguments.user;
if (parsedArguments.user && typeof parsedArguments.user === 'string') {
const basicAuth = parsedArguments.user.split(':')
const username = basicAuth[0] || ''
const password = basicAuth[1] || ''
request.auth = {
mode: 'basic',
basic: {
username,
password
}
}
}
if (Array.isArray(request.data)) {
request.dataArray = request.data;
request.data = request.data.join('&');

View File

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

View File

@ -464,9 +464,10 @@ class Watcher {
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
.on('error', (error) => {
// `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627
// `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached.
// To prevent loops `!forcePolling` is checked.
if (error.code === 'ENOSPC' && !startedNewWatcher && !forcePolling) {
if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {
// This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and
// Multiple watcher being started `startedNewWatcher` is set to prevent this.
startedNewWatcher = true;

View File

@ -1165,12 +1165,22 @@ const registerNetworkIpc = (mainWindow) => {
return `response.${extension}`;
};
const fileName =
getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader();
const getEncodingFormat = () => {
const contentType = getHeaderValue('content-type');
const extension = mime.extension(contentType) || 'txt';
return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64';
};
const determineFileName = () => {
return (
getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader()
);
};
const fileName = determineFileName();
const filePath = await chooseFileToSave(mainWindow, fileName);
if (filePath) {
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64'));
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat()));
}
} catch (error) {
return Promise.reject(error);