mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 13:15:40 +00:00
feat: async parser workers (#3834)
* feat: async parser workers (#3834) --------- Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
@@ -8,7 +8,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEdit, IconTrash, IconFileText } from '@tabler/icons';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,35 +30,89 @@ const Docs = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleDiscardChanges = () => {
|
||||
dispatch(
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: docs
|
||||
})
|
||||
);
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
|
||||
<div className="editing-mode flex justify-between items-center" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2 flex-1 max-h-[70vh]">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary my-6" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
Documentation
|
||||
</div>
|
||||
<div className='flex flex-row gap-2 items-center justify-center'>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
{isEditing ? <IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} /> : <IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />}
|
||||
</div>
|
||||
{/* <div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div> */}
|
||||
{/* <button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
Save
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
value={docs}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
) : (
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
<div className='h-full overflow-auto'>
|
||||
<div className='h-[1px] min-h-[500px]'>
|
||||
{
|
||||
docs?.length > 0 ?
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
:
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
|
||||
|
||||
const documentationPlaceholder = `
|
||||
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.
|
||||
|
||||
## Overview
|
||||
Use this section to provide a high-level overview of your collection. You can describe:
|
||||
- The purpose of these API endpoints
|
||||
- Key features and functionalities
|
||||
- Target audience or users
|
||||
|
||||
## Best Practices
|
||||
- Keep documentation up to date
|
||||
- Include request/response examples
|
||||
- Document error scenarios
|
||||
- Add relevant links and references
|
||||
|
||||
## Markdown Support
|
||||
This documentation supports Markdown formatting! You can use:
|
||||
- **Bold** and *italic* text
|
||||
- \`code blocks\` and syntax highlighting
|
||||
- Tables and lists
|
||||
- [Links](https://example.com)
|
||||
- And more!
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<StyledWrapper className="w-full flex flex-col h-fit">
|
||||
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
@@ -30,6 +30,10 @@ const Info = ({ collection }) => {
|
||||
<td className="py-2 px-2 text-right">Requests :</td>
|
||||
<td className="py-2 px-2">{totalRequestsInCollection}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Size :</td>
|
||||
<td className="py-2 px-2">{collection?.brunoConfig?.size?.toFixed?.(3)} MB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.partial {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.completed {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { flattenItems } from "utils/collections/index";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Docs from "../Docs/index";
|
||||
import Info from "../Info/index";
|
||||
|
||||
const Overview = ({ collection }) => {
|
||||
const flattenedItems = flattenItems(collection.items);
|
||||
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 gap-4">
|
||||
<div className="flex flex-row grid grid-cols-5 w-full gap-8">
|
||||
<div className="col-span-2 flex flex-col gap-12">
|
||||
<Info collection={collection} />
|
||||
{
|
||||
itemsFailedLoading?.length ?
|
||||
<div className="w-full border border-opacity-50 border-yellow-500 rounded-md">
|
||||
<div className="my-2 mx-2 pb-2 font-medium">
|
||||
Following requests were not loaded
|
||||
</div>
|
||||
<table className="w-full border-collapse mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<div className="ml-2">
|
||||
Pathname
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="ml-2">
|
||||
Size
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{flattenedItems?.map(item => (
|
||||
<>
|
||||
{
|
||||
item?.partial && !item?.loading ?
|
||||
<tr className="">
|
||||
<td className="py-2 px-2">{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}</td>
|
||||
<td className="py-2 px-2 text-left">{item?.size?.toFixed?.(2)} MB</td>
|
||||
</tr>
|
||||
: null
|
||||
}
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Docs collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overview;
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
// max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
|
||||
@@ -18,6 +18,7 @@ import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import Overview from './Overview/index';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -97,6 +98,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'overview': {
|
||||
return <Overview collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
@@ -146,6 +150,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
Overview
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
|
||||
@@ -177,13 +184,13 @@ const CollectionSettings = ({ collection }) => {
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
{/* <div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
{hasDocs && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('info')} role="tab" onClick={() => setTab('info')}>
|
||||
Info
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<section className="mt-4 h-full">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
|
||||
const RequestIsLoading = ({ item }) => {
|
||||
return <>
|
||||
<div className='flex flex-col gap-6 w-fit pt-4 pb-3 px-4'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Name</div>
|
||||
<div>{item?.name}</div>
|
||||
</div>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Size</div>
|
||||
<div>{item?.size?.toFixed?.(2)} MB</div>
|
||||
</div>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Path</div>
|
||||
<div>{item?.pathname}</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-6 w-fit justify-start'>
|
||||
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default RequestIsLoading;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import { loadRequest, loadRequestSync } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const RequestNotLoaded = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleLoadRequest = () => {
|
||||
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
const handleLoadRequestSync = () => {
|
||||
!item?.loading && dispatch(loadRequestSync({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className='flex flex-col gap-6 w-fit pt-4 pb-3 px-4'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Name</div>
|
||||
<div>{item?.name}</div>
|
||||
</div>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Size</div>
|
||||
<div>{item?.size?.toFixed?.(2)} MB</div>
|
||||
</div>
|
||||
<div className='flex flex-row gap-1'>
|
||||
<div className='opacity-70 min-w-[50px]'>Path</div>
|
||||
<div>{item?.pathname}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-6 w-fit justify-start'>
|
||||
<div className='flex flex-col'>
|
||||
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequestSync}>
|
||||
{item?.loading ? `Loading Request` : `Load Request`}
|
||||
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
|
||||
</button>
|
||||
<small className='text-muted mt-1'>
|
||||
May cause the app to freeze temporarily while it runs.
|
||||
</small>
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequest}>
|
||||
{item?.loading ? `Loading Request` : `Load Request in Background`}
|
||||
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
|
||||
</button>
|
||||
<small className='text-muted mt-1'>
|
||||
Runs in background.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default RequestNotLoaded;
|
||||
@@ -45,6 +45,10 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.partial-request-overlay {
|
||||
background: ${(props) => props.theme.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -22,6 +22,9 @@ import SecuritySettings from 'components/SecuritySettings';
|
||||
import FolderSettings from 'components/FolderSettings';
|
||||
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
|
||||
import { produce } from 'immer';
|
||||
import CollectionLoadStats from 'components/CollectionSettings/Overview/index';
|
||||
import RequestNotLoaded from './RequestNotLoaded/index';
|
||||
import RequestIsLoading from './RequestIsLoading/index';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -153,6 +156,11 @@ const RequestTabPanel = () => {
|
||||
if (focusedTab.type === 'collection-settings') {
|
||||
return <CollectionSettings collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'collection-overview') {
|
||||
return <CollectionLoadStats collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
@@ -167,6 +175,14 @@ const RequestTabPanel = () => {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
}
|
||||
|
||||
if (item?.partial) {
|
||||
return <RequestNotLoaded item={item} collection={collection} />
|
||||
}
|
||||
|
||||
if (item?.loading) {
|
||||
return <RequestIsLoading item={item} />
|
||||
}
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
|
||||
@@ -13,6 +13,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-overview': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'security-settings': {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -70,7 +70,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
};
|
||||
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.partial {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,17 @@
|
||||
import RequestMethod from "../RequestMethod";
|
||||
import { IconLoader2, IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
|
||||
const CollectionItemIcon = ({ item }) => {
|
||||
if (item?.loading) {
|
||||
return <IconLoader2 className="animate-spin w-fit mr-2" size={18} strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
if (item?.partial) {
|
||||
return <StyledWrapper><IconAlertTriangle size={18} className="w-fit mr-2 partial" strokeWidth={1.5} /></StyledWrapper>;
|
||||
}
|
||||
|
||||
return <RequestMethod item={item} />;
|
||||
};
|
||||
|
||||
export default CollectionItemIcon;
|
||||
@@ -11,7 +11,6 @@ import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import RequestMethod from './RequestMethod';
|
||||
import RenameCollectionItem from './RenameCollectionItem';
|
||||
import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
@@ -24,7 +23,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import { uuid } from 'utils/common';
|
||||
import CollectionItemIcon from './CollectionItemIcon/index';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -294,12 +293,12 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="ml-1 flex items-center overflow-hidden flex-1"
|
||||
className="ml-1 flex w-full h-full items-center overflow-hidden"
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<RequestMethod item={item} />
|
||||
<CollectionItemIcon item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
@@ -421,4 +420,4 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionItem;
|
||||
export default CollectionItem;
|
||||
@@ -3,10 +3,10 @@ import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { loadCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -15,12 +15,12 @@ import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import ExportCollection from './ExportCollection';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
import { isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
|
||||
import RenameCollection from './RenameCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CloneCollection from './CloneCollection/index';
|
||||
import { areItemsLoading } from 'utils/collections/index';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
@@ -30,7 +30,9 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const [hasCollectionLoadingBeenTriggered, setHasCollectionLoadingBeenTriggered] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
@@ -70,6 +72,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
|
||||
const handleCollapseCollection = () => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
setHasCollectionLoadingBeenTriggered(true);
|
||||
!hasCollectionLoadingBeenTriggered && dispatch(loadCollection({ collectionUid: collection?.uid, collectionPathname: collection?.pathname, brunoConfig: collection?.brunoConfig }));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
@@ -165,6 +169,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
<div className="ml-1" id="sidebar-collection-name">
|
||||
{collection.name}
|
||||
</div>
|
||||
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
|
||||
</div>
|
||||
<div className="collection-actions">
|
||||
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
|
||||
@@ -184,7 +184,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.36.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.36.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,7 +161,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
if (!folder) {
|
||||
return reject(new Error('Folder not found'));
|
||||
}
|
||||
console.log(collection);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
@@ -170,7 +169,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
pathname: folder.pathname,
|
||||
root: folder.root
|
||||
};
|
||||
console.log(folderData);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
@@ -1192,4 +1190,27 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request-init', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRequestSync = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request-init', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
ipcRenderer.invoke('renderer:load-request-sync', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-collection', { collectionUid, collectionPathname, brunoConfig }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export const collectionsSlice = createSlice({
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
collection.settingsSelectedTab = 'headers';
|
||||
collection.settingsSelectedTab = 'overview';
|
||||
collection.folderLevelSettingsSelectedTab = {};
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
@@ -1582,7 +1582,7 @@ export const collectionsSlice = createSlice({
|
||||
name: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: []
|
||||
items: [],
|
||||
};
|
||||
currentSubItems.push(childItem);
|
||||
}
|
||||
@@ -1604,6 +1604,9 @@ export const collectionsSlice = createSlice({
|
||||
currentItem.filename = file.meta.name;
|
||||
currentItem.pathname = file.meta.pathname;
|
||||
currentItem.draft = null;
|
||||
currentItem.partial = file.partial;
|
||||
currentItem.loading = file.loading;
|
||||
currentItem.size = file.size;
|
||||
} else {
|
||||
currentSubItems.push({
|
||||
uid: file.data.uid,
|
||||
@@ -1613,7 +1616,10 @@ export const collectionsSlice = createSlice({
|
||||
request: file.data.request,
|
||||
filename: file.meta.name,
|
||||
pathname: file.meta.pathname,
|
||||
draft: null
|
||||
draft: null,
|
||||
partial: file.partial,
|
||||
loading: file.loading,
|
||||
size: file.size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const tabsSlice = createSlice({
|
||||
}
|
||||
|
||||
if (
|
||||
['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
|
||||
['variables', 'collection-settings', 'collection-overview', 'collection-runner', 'security-settings'].includes(action.payload.type)
|
||||
) {
|
||||
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
|
||||
if (tab) {
|
||||
|
||||
@@ -136,6 +136,16 @@ export const findEnvironmentInCollectionByName = (collection, name) => {
|
||||
return find(collection.environments, (e) => e.name === name);
|
||||
};
|
||||
|
||||
export const areItemsLoading = (folder) => {
|
||||
let flattenedItems = flattenItems(folder.items);
|
||||
return flattenedItems?.reduce((isLoading, i) => {
|
||||
if (i?.loading) {
|
||||
isLoading = true;
|
||||
}
|
||||
return isLoading;
|
||||
}, false);
|
||||
}
|
||||
|
||||
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
@@ -991,4 +1001,4 @@ const mergeVars = (collection, requestTreePath = []) => {
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { dialog, ipcMain } = require('electron');
|
||||
const Yup = require('yup');
|
||||
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
|
||||
const { isDirectory, normalizeAndResolvePath, addCollectionStatsToBrunoConfig } = require('../utils/filesystem');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
|
||||
// todo: bruno.json config schema validation errors must be propagated to the UI
|
||||
@@ -59,7 +59,7 @@ const openCollectionDialog = async (win, watcher) => {
|
||||
const openCollection = async (win, watcher, collectionPath, options = {}) => {
|
||||
if (!watcher.hasWatcher(collectionPath)) {
|
||||
try {
|
||||
const brunoConfig = await getCollectionConfigFile(collectionPath);
|
||||
let brunoConfig = await getCollectionConfigFile(collectionPath);
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
|
||||
if (!brunoConfig.ignore || brunoConfig.ignore.length === 0) {
|
||||
@@ -69,6 +69,7 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => {
|
||||
// this is to maintain backwards compatibility with older collections
|
||||
brunoConfig.ignore = ['node_modules', '.git'];
|
||||
}
|
||||
brunoConfig = await addCollectionStatsToBrunoConfig({ brunoConfig, collectionPath });
|
||||
|
||||
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig);
|
||||
|
||||
@@ -2,8 +2,8 @@ const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, collectionBruToJson } = require('../bru');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, collectionBruToJson, bruToJsonSync } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
|
||||
const { uuid } = require('../utils/common');
|
||||
@@ -13,6 +13,9 @@ const { setDotEnvVars } = require('../store/process-env');
|
||||
const { setBrunoConfig } = require('../store/bruno-config');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const UiStateSnapshot = require('../store/ui-state-snapshot');
|
||||
const { getBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
|
||||
|
||||
const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
@@ -44,28 +47,6 @@ const isCollectionRootBruFile = (pathname, collectionPath) => {
|
||||
return dirname === collectionPath && basename === 'collection.bru';
|
||||
};
|
||||
|
||||
const hydrateRequestWithUuid = (request, pathname) => {
|
||||
request.uid = getRequestUid(pathname);
|
||||
|
||||
const params = _.get(request, 'request.params', []);
|
||||
const headers = _.get(request, 'request.headers', []);
|
||||
const requestVars = _.get(request, 'request.vars.req', []);
|
||||
const responseVars = _.get(request, 'request.vars.res', []);
|
||||
const assertions = _.get(request, 'request.assertions', []);
|
||||
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
|
||||
|
||||
params.forEach((param) => (param.uid = uuid()));
|
||||
headers.forEach((header) => (header.uid = uuid()));
|
||||
requestVars.forEach((variable) => (variable.uid = uuid()));
|
||||
responseVars.forEach((variable) => (variable.uid = uuid()));
|
||||
assertions.forEach((assertion) => (assertion.uid = uuid()));
|
||||
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
|
||||
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const hydrateBruCollectionFileWithUuid = (collectionRoot) => {
|
||||
const params = _.get(collectionRoot, 'request.params', []);
|
||||
const headers = _.get(collectionRoot, 'request.headers', []);
|
||||
@@ -99,7 +80,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
|
||||
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = bruToEnvJson(bruContent);
|
||||
file.data = await bruToEnvJson(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
|
||||
@@ -134,7 +115,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
|
||||
};
|
||||
|
||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = bruToEnvJson(bruContent);
|
||||
file.data = await bruToEnvJson(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
|
||||
@@ -179,13 +160,13 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
}
|
||||
};
|
||||
|
||||
const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const add = async ({ win, pathname, collectionUid, watchPath: collectionPath, shouldLoadAsync }) => {
|
||||
console.log(`watcher add: ${pathname}`);
|
||||
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const brunoConfig = JSON.parse(content);
|
||||
let brunoConfig = JSON.parse(content);
|
||||
|
||||
setBrunoConfig(collectionUid, brunoConfig);
|
||||
} catch (err) {
|
||||
@@ -228,7 +209,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
@@ -241,7 +222,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
|
||||
// Is this a folder.bru file?
|
||||
if (path.basename(pathname) === 'folder.bru') {
|
||||
console.log('folder.bru file detected');
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
@@ -254,7 +234,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
@@ -274,20 +254,63 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
let fileStats;
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = bruToJson(bruContent);
|
||||
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
if (shouldLoadAsync) {
|
||||
try {
|
||||
const fileStats = fs.statSync(pathname);
|
||||
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
if (fileStats.size < MAX_FILE_SIZE) {
|
||||
file.data = metaJson;
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
file.data = await bruToJson(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = false;
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
file.data = {};
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
}
|
||||
else {
|
||||
file.data = bruToJsonSync(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
const addDirectory = ({ win, pathname, collectionUid, watchPath: collectionPath }) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (pathname === envDirectory) {
|
||||
@@ -304,7 +327,7 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
|
||||
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
|
||||
};
|
||||
|
||||
const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const change = async ({ win, pathname, collectionUid, watchPath: collectionPath }) => {
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
@@ -357,7 +380,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
return;
|
||||
@@ -378,7 +401,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
};
|
||||
|
||||
const bru = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = bruToJson(bru);
|
||||
file.data = await bruToJson(bru);
|
||||
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
@@ -388,7 +411,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
const unlink = ({ win, pathname, collectionUid, watchPath: collectionPath }) => {
|
||||
console.log(`watcher unlink: ${pathname}`);
|
||||
|
||||
if (isBruEnvironmentConfig(pathname, collectionPath)) {
|
||||
@@ -407,7 +430,7 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
const unlinkDir = ({ win, pathname, collectionUid, watchPath: collectionPath }) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (pathname === envDirectory) {
|
||||
@@ -424,7 +447,7 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||
};
|
||||
|
||||
const onWatcherSetupComplete = (win, collectionPath) => {
|
||||
const onWatcherSetupComplete = ({ win, watchPath: collectionPath }) => {
|
||||
const UiStateSnapshotStore = new UiStateSnapshot();
|
||||
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
|
||||
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath);
|
||||
@@ -436,7 +459,7 @@ class Watcher {
|
||||
this.watchers = {};
|
||||
}
|
||||
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, shouldLoadAsync) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
@@ -466,12 +489,12 @@ class Watcher {
|
||||
|
||||
let startedNewWatcher = false;
|
||||
watcher
|
||||
.on('ready', () => onWatcherSetupComplete(win, watchPath))
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
|
||||
.on('ready', () => onWatcherSetupComplete({ win, watchPath }))
|
||||
.on('add', (pathname) => add({win, pathname, collectionUid, watchPath, shouldLoadAsync }))
|
||||
.on('addDir', (pathname) => addDirectory({ win, pathname, collectionUid, watchPath, shouldLoadAsync }))
|
||||
.on('change', (pathname) => change({ win, pathname, collectionUid, watchPath, shouldLoadAsync }))
|
||||
.on('unlink', (pathname) => unlink({ win, pathname, collectionUid, watchPath, shouldLoadAsync }))
|
||||
.on('unlinkDir', (pathname) => unlinkDir({ win, pathname, collectionUid, watchPath, shouldLoadAsync }))
|
||||
.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.
|
||||
@@ -488,7 +511,7 @@ class Watcher {
|
||||
'Update you system config to allow more concurrently watched files with:',
|
||||
'"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
|
||||
);
|
||||
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true);
|
||||
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, shouldLoadAsync);
|
||||
} else {
|
||||
console.error(`An error occurred in the watcher for: ${watchPath}`, error);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
bruToJsonV2,
|
||||
jsonToBruV2,
|
||||
bruToEnvJsonV2,
|
||||
envJsonToBruV2,
|
||||
collectionBruToJson: _collectionBruToJson,
|
||||
jsonToCollectionBru: _jsonToCollectionBru
|
||||
} = require('@usebruno/lang');
|
||||
const { bruToJsonV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
|
||||
const BruWorker = require('./workers');
|
||||
|
||||
const collectionBruToJson = (bru) => {
|
||||
// collections can have bru files of varying sizes. we use two worker threads:
|
||||
// - one thread handles smaller files (<0.1MB), so they get processed quickly and show up in the gui faster.
|
||||
// - the other thread takes care of larger files (>=0.1MB). Splitting the processing like this helps with parsing performance.
|
||||
const bruWorker = new BruWorker({
|
||||
lanes: [{
|
||||
maxSize: 0.1
|
||||
},{
|
||||
maxSize: 100
|
||||
}]
|
||||
});
|
||||
|
||||
const collectionBruToJson = async (bru) => {
|
||||
try {
|
||||
const json = _collectionBruToJson(bru);
|
||||
const json = await bruWorker?.collectionBruToJson(bru);
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
@@ -38,7 +43,7 @@ const collectionBruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
const jsonToCollectionBru = (json, isFolder) => {
|
||||
const jsonToCollectionBru = async (json, isFolder) => {
|
||||
try {
|
||||
const collectionBruJson = {
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
@@ -67,13 +72,14 @@ const jsonToCollectionBru = (json, isFolder) => {
|
||||
collectionBruJson.auth = _.get(json, 'request.auth', {});
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
const bru = await bruWorker?.jsonToCollectionBru(collectionBruJson);
|
||||
return bru;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const bruToEnvJson = (bru) => {
|
||||
const bruToEnvJson = async (bru) => {
|
||||
try {
|
||||
const json = bruToEnvJsonV2(bru);
|
||||
|
||||
@@ -90,7 +96,7 @@ const bruToEnvJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
const envJsonToBru = (json) => {
|
||||
const envJsonToBru = async (json) => {
|
||||
try {
|
||||
const bru = envJsonToBruV2(json);
|
||||
return bru;
|
||||
@@ -108,9 +114,15 @@ const envJsonToBru = (json) => {
|
||||
* @param {string} bru The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
const bruToJson = (bru) => {
|
||||
const bruToJson = async (data, parsed = false) => {
|
||||
try {
|
||||
const json = bruToJsonV2(bru);
|
||||
let json;
|
||||
if (parsed) {
|
||||
json = data;
|
||||
}
|
||||
else {
|
||||
json = await bruWorker?.bruToJson(data);
|
||||
}
|
||||
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
@@ -149,6 +161,64 @@ const bruToJson = (bru) => {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a BRU file to JSON.
|
||||
*
|
||||
* We map the json response from the bru lang and transform it into the DSL
|
||||
* format that the app uses
|
||||
*
|
||||
* @param {string} bru The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
const bruToJsonSync = (data, parsed = false) => {
|
||||
try {
|
||||
let json;
|
||||
if (parsed) {
|
||||
json = data;
|
||||
}
|
||||
else {
|
||||
json = bruToJsonV2(data);
|
||||
}
|
||||
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
requestType = 'http-request';
|
||||
} else if (requestType === 'graphql') {
|
||||
requestType = 'graphql-request';
|
||||
} else {
|
||||
requestType = 'http-request';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
const transformedJson = {
|
||||
type: requestType,
|
||||
name: _.get(json, 'meta.name'),
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1,
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
tests: _.get(json, 'tests', ''),
|
||||
docs: _.get(json, 'docs', '')
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
|
||||
return transformedJson;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a JSON to BRU file.
|
||||
*
|
||||
@@ -158,7 +228,7 @@ const bruToJson = (bru) => {
|
||||
* @param {object} json The JSON representation of the BRU file.
|
||||
* @returns {string} The BRU file content.
|
||||
*/
|
||||
const jsonToBru = (json) => {
|
||||
const jsonToBru = async (json) => {
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = 'http';
|
||||
@@ -195,11 +265,13 @@ const jsonToBru = (json) => {
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
};
|
||||
|
||||
return jsonToBruV2(bruJson);
|
||||
const bru = await bruWorker?.jsonToBru(bruJson)
|
||||
return bru;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToJsonSync,
|
||||
jsonToBru,
|
||||
bruToEnvJson,
|
||||
envJsonToBru,
|
||||
|
||||
43
packages/bruno-electron/src/bru/workers/index.js
Normal file
43
packages/bruno-electron/src/bru/workers/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const WorkerQueue = require("../../workers");
|
||||
const path = require("path");
|
||||
|
||||
const getSize = (data) => {
|
||||
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8');
|
||||
}
|
||||
|
||||
class BruWorker {
|
||||
constructor({ lanes = [] }) {
|
||||
this.workerQueues = lanes?.map(lane => ({
|
||||
maxSize: lane?.maxSize,
|
||||
workerQueue: new WorkerQueue()
|
||||
}));
|
||||
}
|
||||
|
||||
getWorkerQueue(size) {
|
||||
return this.workerQueues.find((wq) => wq?.maxSize >= size)?.workerQueue || this.workerQueues.at(-1)?.workerQueue;
|
||||
}
|
||||
|
||||
async enqueueTask({data, scriptFile }) {
|
||||
const size = getSize(data);
|
||||
const workerQueue = this.getWorkerQueue(size);
|
||||
return workerQueue.enqueue({ data, priority: size, scriptPath: path.join(__dirname, `./scripts/${scriptFile}.js`) });
|
||||
}
|
||||
|
||||
async bruToJson(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `bru-to-json` });
|
||||
}
|
||||
|
||||
async jsonToBru(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `json-to-bru` });
|
||||
}
|
||||
|
||||
async collectionBruToJson(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `collection-bru-to-json` });
|
||||
}
|
||||
|
||||
async jsonToCollectionBru(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `json-to-collection-bru` });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BruWorker;
|
||||
@@ -0,0 +1,13 @@
|
||||
const { workerData, parentPort } = require('worker_threads');
|
||||
const {
|
||||
bruToJsonV2,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
try {
|
||||
const bru = workerData;
|
||||
const json = bruToJsonV2(bru);
|
||||
parentPort.postMessage(json);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const { workerData, parentPort } = require('worker_threads');
|
||||
const {
|
||||
collectionBruToJson,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
try {
|
||||
const bru = workerData;
|
||||
const json = collectionBruToJson(bru);
|
||||
parentPort.postMessage(json);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const { workerData, parentPort } = require('worker_threads');
|
||||
const {
|
||||
jsonToBruV2,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
try {
|
||||
const json = workerData;
|
||||
const bru = jsonToBruV2(json);
|
||||
parentPort.postMessage(bru);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const { workerData, parentPort } = require('worker_threads');
|
||||
const {
|
||||
jsonToCollectionBru,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
try {
|
||||
const json = workerData;
|
||||
const bru = jsonToCollectionBru(json);
|
||||
parentPort.postMessage(bru);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { ipcMain, shell, dialog, app } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru, bruToJsonSync } = require('../bru');
|
||||
|
||||
const {
|
||||
isValidPathname,
|
||||
@@ -24,6 +24,9 @@ const {
|
||||
isWindowsOS,
|
||||
isValidFilename,
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB,
|
||||
addCollectionStatsToBrunoConfig
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@@ -32,11 +35,17 @@ const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cook
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const CollectionSecurityStore = require('../store/collection-security');
|
||||
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
|
||||
const { getBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
const collectionSecurityStore = new CollectionSecurityStore();
|
||||
const uiStateSnapshotStore = new UiStateSnapshotStore();
|
||||
|
||||
// size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not.
|
||||
const MAX_COLLECTION_SIZE_IN_MB = 5;
|
||||
const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 2;
|
||||
const MAX_COLLECTION_FILES_COUNT = 100;
|
||||
|
||||
const envHasSecrets = (environment = {}) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||
|
||||
@@ -88,7 +97,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
const uid = generateUidBasedOnHash(dirPath);
|
||||
const brunoConfig = {
|
||||
let brunoConfig = {
|
||||
version: '1',
|
||||
name: collectionName,
|
||||
type: 'collection',
|
||||
@@ -97,6 +106,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const content = await stringifyJson(brunoConfig);
|
||||
await writeFile(path.join(dirPath, 'bruno.json'), content);
|
||||
|
||||
brunoConfig = await addCollectionStatsToBrunoConfig({ brunoConfig, collectionPath: dirPath });
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig);
|
||||
} catch (error) {
|
||||
@@ -126,9 +137,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const brunoJsonFilePath = path.join(previousPath, 'bruno.json');
|
||||
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
|
||||
|
||||
//Change new name of collection
|
||||
let json = JSON.parse(content);
|
||||
json.name = collectionName;
|
||||
// Change new name of collection
|
||||
let brunoConfig = JSON.parse(content);
|
||||
brunoConfig.name = collectionName;
|
||||
const cont = await stringifyJson(json);
|
||||
|
||||
// write the bruno.json to new dir
|
||||
@@ -147,7 +158,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
fs.copyFileSync(sourceFilePath, newFilePath);
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, json);
|
||||
brunoConfig = await addCollectionStatsToBrunoConfig({ brunoConfig, collectionPath: dirPath });
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
}
|
||||
);
|
||||
@@ -184,7 +197,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
name: folderName
|
||||
};
|
||||
|
||||
const content = jsonToCollectionBru(
|
||||
const content = await jsonToCollectionBru(
|
||||
folderRoot,
|
||||
true // isFolder
|
||||
);
|
||||
@@ -197,7 +210,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
try {
|
||||
const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');
|
||||
|
||||
const content = jsonToCollectionBru(collectionRoot);
|
||||
const content = await jsonToCollectionBru(collectionRoot);
|
||||
await writeFile(collectionBruFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -213,7 +226,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (!isValidFilename(request.name)) {
|
||||
throw new Error(`path: ${request.name}.bru is not a valid filename`);
|
||||
}
|
||||
const content = jsonToBru(request);
|
||||
const content = await jsonToBru(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -227,7 +240,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = jsonToBru(request);
|
||||
const content = await jsonToBru(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -245,7 +258,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = jsonToBru(request);
|
||||
const content = await jsonToBru(request);
|
||||
await writeFile(pathname, content);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -275,7 +288,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const content = envJsonToBru(environment);
|
||||
const content = await envJsonToBru(environment);
|
||||
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
@@ -300,7 +313,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const content = envJsonToBru(environment);
|
||||
const content = await envJsonToBru(environment);
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -412,11 +425,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// update name in file and save new copy, then delete old copy
|
||||
const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read
|
||||
const jsonData = bruToJson(data);
|
||||
const jsonData = await bruToJson(data);
|
||||
jsonData.name = newName;
|
||||
moveRequestUid(oldPath, newPath);
|
||||
|
||||
const content = jsonToBru(jsonData);
|
||||
const content = await jsonToBru(jsonData);
|
||||
await fs.promises.unlink(oldPath);
|
||||
await writeFile(newPath, content);
|
||||
|
||||
@@ -516,9 +529,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// Recursive function to parse the collection items and create files/folders
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach((item) => {
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = jsonToBru(item);
|
||||
const content = await jsonToBru(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
@@ -529,7 +542,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
if (item?.root?.meta?.name) {
|
||||
const folderBruFilePath = path.join(folderPath, 'folder.bru');
|
||||
const folderContent = jsonToCollectionBru(
|
||||
const folderContent = await jsonToCollectionBru(
|
||||
item.root,
|
||||
true // isFolder
|
||||
);
|
||||
@@ -554,8 +567,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
fs.mkdirSync(envDirPath);
|
||||
}
|
||||
|
||||
environments.forEach((env) => {
|
||||
const content = envJsonToBru(env);
|
||||
environments.forEach(async (env) => {
|
||||
const content = await envJsonToBru(env);
|
||||
const filePath = path.join(envDirPath, `${env.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
});
|
||||
@@ -579,15 +592,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
await createDirectory(collectionPath);
|
||||
|
||||
const uid = generateUidBasedOnHash(collectionPath);
|
||||
const brunoConfig = getBrunoJsonConfig(collection);
|
||||
let brunoConfig = getBrunoJsonConfig(collection);
|
||||
const stringifiedBrunoConfig = await stringifyJson(brunoConfig);
|
||||
|
||||
// Write the Bruno configuration to a file
|
||||
await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);
|
||||
|
||||
const collectionContent = jsonToCollectionBru(collection.root);
|
||||
const collectionContent = await jsonToCollectionBru(collection.root);
|
||||
await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);
|
||||
|
||||
brunoConfig = await addCollectionStatsToBrunoConfig({ brunoConfig, collectionPath });
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);
|
||||
|
||||
@@ -609,9 +624,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// Recursive function to parse the folder and create files/folders
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach((item) => {
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = jsonToBru(item);
|
||||
const content = await jsonToBru(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
@@ -621,7 +636,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// If folder has a root element, then I should write its folder.bru file
|
||||
if (item.root) {
|
||||
const folderContent = jsonToCollectionBru(item.root, true);
|
||||
const folderContent = await jsonToCollectionBru(item.root, true);
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(folderPath, `folder.bru`);
|
||||
fs.writeFileSync(bruFolderPath, folderContent);
|
||||
@@ -639,7 +654,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// If initial folder has a root element, then I should write its folder.bru file
|
||||
if (itemFolder.root) {
|
||||
const folderContent = jsonToCollectionBru(itemFolder.root, true);
|
||||
const folderContent = await jsonToCollectionBru(itemFolder.root, true);
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(collectionPath, `folder.bru`);
|
||||
fs.writeFileSync(bruFolderPath, folderContent);
|
||||
@@ -655,13 +670,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
|
||||
try {
|
||||
for (let item of itemsToResequence) {
|
||||
for await (let item of itemsToResequence) {
|
||||
const bru = fs.readFileSync(item.pathname, 'utf8');
|
||||
const jsonData = bruToJson(bru);
|
||||
const jsonData = await bruToJson(bru);
|
||||
|
||||
if (jsonData.seq !== item.seq) {
|
||||
jsonData.seq = item.seq;
|
||||
const content = jsonToBru(jsonData);
|
||||
const content = await jsonToBru(jsonData);
|
||||
await writeFile(item.pathname, content);
|
||||
}
|
||||
}
|
||||
@@ -776,6 +791,116 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request-init', async (event, { collectionUid, pathname }) => {
|
||||
let fileStats;
|
||||
try {
|
||||
fileStats = fs.statSync(pathname);
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
|
||||
file.data = metaJson;
|
||||
file.loading = true;
|
||||
file.partial = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
|
||||
let fileStats;
|
||||
try {
|
||||
fileStats = fs.statSync(pathname);
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = await bruToJson(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
} catch (error) {
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request-sync', async (event, { collectionUid, pathname }) => {
|
||||
let fileStats;
|
||||
try {
|
||||
fileStats = fs.statSync(pathname);
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = bruToJsonSync(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
} catch (error) {
|
||||
if (hasBruExtension(pathname)) {
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname)
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
@@ -790,8 +915,7 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
|
||||
shell.openExternal(docsURL);
|
||||
});
|
||||
|
||||
ipcMain.on('main:collection-opened', (win, pathname, uid, brunoConfig) => {
|
||||
watcher.addWatcher(win, pathname, uid, brunoConfig);
|
||||
ipcMain.on('main:collection-opened', async (win, pathname, uid, brunoConfig) => {
|
||||
lastOpenedCollections.add(pathname);
|
||||
app.addRecentDocument(pathname);
|
||||
});
|
||||
@@ -801,6 +925,12 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
|
||||
mainWindow.webContents.send('main:start-quit-flow');
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
|
||||
const { size: collectionSize, filesCount: collectionBruFilesCount, maxFileSize: maxSingleBruFileSize } = await getCollectionStats(collectionPathname);
|
||||
const shouldLoadCollectionAsync = (collectionSize > MAX_COLLECTION_SIZE_IN_MB) || (collectionBruFilesCount > MAX_COLLECTION_FILES_COUNT) || (maxSingleBruFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB);
|
||||
watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);
|
||||
});
|
||||
|
||||
ipcMain.handle('main:complete-quit-flow', () => {
|
||||
mainWindow.destroy();
|
||||
});
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { uuid } = require('./common');
|
||||
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
|
||||
@@ -203,6 +207,51 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const getBruFileMeta = (data) => {
|
||||
try {
|
||||
const metaRegex = /meta\s*{\s*([\s\S]*?)\s*}/;
|
||||
const match = data.match(metaRegex);
|
||||
if (match) {
|
||||
const metaContent = match[1].trim();
|
||||
const lines = metaContent.replace(/\r\n/g, '\n').split('\n');
|
||||
const metaJson = {};
|
||||
lines.forEach(line => {
|
||||
const [key, value] = line.split(':').map(str => str.trim());
|
||||
if (key && value) {
|
||||
metaJson[key] = isNaN(value) ? value : Number(value);
|
||||
}
|
||||
});
|
||||
return { meta: metaJson };
|
||||
} else {
|
||||
console.log('No "meta" block found in the file.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error reading file:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const hydrateRequestWithUuid = (request, pathname) => {
|
||||
request.uid = getRequestUid(pathname);
|
||||
|
||||
const params = get(request, 'request.params', []);
|
||||
const headers = get(request, 'request.headers', []);
|
||||
const requestVars = get(request, 'request.vars.req', []);
|
||||
const responseVars = get(request, 'request.vars.res', []);
|
||||
const assertions = get(request, 'request.assertions', []);
|
||||
const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = get(request, 'request.body.multipartForm', []);
|
||||
|
||||
params.forEach((param) => (param.uid = uuid()));
|
||||
headers.forEach((header) => (header.uid = uuid()));
|
||||
requestVars.forEach((variable) => (variable.uid = uuid()));
|
||||
responseVars.forEach((variable) => (variable.uid = uuid()));
|
||||
assertions.forEach((assertion) => (assertion.uid = uuid()));
|
||||
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
|
||||
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const slash = (path) => {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
|
||||
if (isExtendedLengthPath) {
|
||||
@@ -221,13 +270,18 @@ const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
return findItemByPathname(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
getTreePathFromCollectionToItem,
|
||||
flattenItems,
|
||||
findItem,
|
||||
findItemInCollection,
|
||||
slash,
|
||||
findItemByPathname,
|
||||
findItemInCollectionByPathname
|
||||
}
|
||||
findItemInCollectionByPathname,
|
||||
findParentItemInCollection,
|
||||
getBruFileMeta,
|
||||
hydrateRequestWithUuid
|
||||
};
|
||||
@@ -211,6 +211,57 @@ const safeToRename = (oldPath, newPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getCollectionStats = async (directoryPath) => {
|
||||
let size = 0;
|
||||
let filesCount = 0;
|
||||
let maxFileSize = 0;
|
||||
|
||||
async function calculateStats(directory) {
|
||||
const entries = await fsPromises.readdir(directory, { withFileTypes: true });
|
||||
|
||||
const tasks = entries.map(async (entry) => {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (['node_modules', '.git'].includes(entry.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await calculateStats(fullPath);
|
||||
}
|
||||
|
||||
if (path.extname(fullPath) === '.bru') {
|
||||
const stats = await fsPromises.stat(fullPath);
|
||||
size += stats?.size;
|
||||
if (maxFileSize < stats?.size) {
|
||||
maxFileSize = stats?.size;
|
||||
}
|
||||
filesCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
await calculateStats(directoryPath);
|
||||
|
||||
size = sizeInMB(size);
|
||||
maxFileSize = sizeInMB(maxFileSize);
|
||||
|
||||
return { size, filesCount, maxFileSize };
|
||||
}
|
||||
|
||||
const sizeInMB = (size) => {
|
||||
return size / (1024 * 1024);
|
||||
}
|
||||
|
||||
const addCollectionStatsToBrunoConfig = async ({ brunoConfig, collectionPath }) => {
|
||||
const { size: collectionSize, filesCount: collectionBruFilesCount } = await getCollectionStats(collectionPath);
|
||||
brunoConfig.size = collectionSize;
|
||||
brunoConfig.filesCount = collectionBruFilesCount;
|
||||
return brunoConfig;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidPathname,
|
||||
exists,
|
||||
@@ -235,5 +286,8 @@ module.exports = {
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename,
|
||||
hasSubDirectories
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB,
|
||||
addCollectionStatsToBrunoConfig
|
||||
};
|
||||
|
||||
52
packages/bruno-electron/src/workers/index.js
Normal file
52
packages/bruno-electron/src/workers/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
class WorkerQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
async enqueue({ priority, scriptPath, data }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ priority, scriptPath, data, resolve, reject });
|
||||
this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority);
|
||||
this.processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
if (this.isProcessing || this.queue.length === 0) return;
|
||||
this.isProcessing = true;
|
||||
const { scriptPath, data, resolve, reject } = this.queue.shift();
|
||||
try {
|
||||
const result = await this.runWorker({ scriptPath, data });
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async runWorker({ scriptPath, data }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(scriptPath, { workerData: data });
|
||||
worker.on('message', (data) => {
|
||||
resolve(data);
|
||||
worker.terminate();
|
||||
});
|
||||
worker.on('error', (error) => {
|
||||
reject(error);
|
||||
worker.terminate();
|
||||
});
|
||||
worker.on('exit', (code) => {
|
||||
// if (code !== 0)
|
||||
reject(new Error(`stopped with ${code} exit code`));
|
||||
worker.terminate();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkerQueue;
|
||||
Reference in New Issue
Block a user