mirror of
https://github.com/usebruno/bruno.git
synced 2025-02-02 02:49:48 +01:00
Merge branch 'main' into bugfix/chevron-collapse
This commit is contained in:
commit
66bc6e1669
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -52,6 +52,9 @@ jobs:
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
50
package-lock.json
generated
50
package-lock.json
generated
@ -24030,7 +24030,6 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "1.7.5",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
@ -24147,7 +24146,7 @@
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
@ -24168,6 +24167,16 @@
|
||||
"bru": "bin/bru.js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@ -24210,7 +24219,7 @@
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
@ -24247,6 +24256,17 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@ -24341,7 +24361,7 @@
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
@ -24368,6 +24388,16 @@
|
||||
"@usebruno/vm2": "^3.9.13"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@ -24445,7 +24475,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"body-parser": "1.20.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
@ -24459,6 +24489,16 @@
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/fast-xml-parser": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
|
@ -20,7 +20,6 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "1.7.5",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
|
@ -8,6 +8,8 @@ const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* Removes the glow outline around the folded json */
|
||||
@ -26,6 +28,10 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
|
@ -194,8 +194,20 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Ctrl-/': 'toggleComment',
|
||||
'Cmd-/': 'toggleComment'
|
||||
'Ctrl-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Cmd-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
@ -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, IconX, IconFileText } from '@tabler/icons';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -29,35 +30,95 @@ 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));
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
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 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'>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2 flex-1 max-h-[70vh]">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
value={docs || ''}
|
||||
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>
|
||||
) : (
|
||||
<div className='h-full overflow-auto pl-1'>
|
||||
<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!
|
||||
`;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
@ -1,13 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
td {
|
||||
&:first-child {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Name :</td>
|
||||
<td className="py-2 px-2">{collection.name}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Location :</td>
|
||||
<td className="py-2 px-2 break-all">{collection.pathname}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Ignored files :</td>
|
||||
<td className="py-2 px-2 break-all">{collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Environments :</td>
|
||||
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Requests :</td>
|
||||
<td className="py-2 px-2">{totalRequestsInCollection}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
<div className="grid gap-6">
|
||||
{/* Location Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Location</div>
|
||||
<div className="mt-1 text-sm text-muted break-all">
|
||||
{collection.pathname}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environments Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Environments</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requests Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Requests</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
{totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.card {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.table {
|
||||
thead {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { flattenItems } from "utils/collections";
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const flattenedItems = flattenItems(collection.items);
|
||||
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
|
||||
|
||||
if (!itemsFailedLoading?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-3 text-left font-medium">
|
||||
Pathname
|
||||
</th>
|
||||
<th className="py-2 px-3 text-left font-medium">
|
||||
Size
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{flattenedItems?.map((item, index) => (
|
||||
item?.partial && !item?.loading ? (
|
||||
<tr key={index}>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
|
||||
</td>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.size?.toFixed?.(2)} MB
|
||||
</td>
|
||||
</tr>
|
||||
) : null
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestsNotLoaded;
|
@ -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,27 @@
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Docs from "../Docs";
|
||||
import Info from "./Info";
|
||||
import { IconBox } from '@tabler/icons';
|
||||
import RequestsNotLoaded from "./RequestsNotLoaded";
|
||||
|
||||
const Overview = ({ collection }) => {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="grid grid-cols-5 gap-4 h-full">
|
||||
<div className="col-span-2">
|
||||
<div className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconBox size={24} stroke={1.5} />
|
||||
{collection?.name}
|
||||
</div>
|
||||
<Info collection={collection} />
|
||||
<RequestsNotLoaded collection={collection} />
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Docs collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overview;
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
.settings-label {
|
||||
width: 110px;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
|
@ -12,12 +12,11 @@ import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import Overview from './Overview/index';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@ -97,6 +96,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'overview': {
|
||||
return <Overview collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
@ -128,12 +130,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'docs': {
|
||||
return <Docs collection={collection} />;
|
||||
}
|
||||
case 'info': {
|
||||
return <Info collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -146,6 +142,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 +176,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<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>
|
||||
<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};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -9,7 +9,6 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
h1 {
|
||||
@ -80,12 +79,6 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledMarkdownBodyWrapper;
|
||||
|
@ -62,7 +62,7 @@ const Modal = ({
|
||||
confirmText,
|
||||
cancelText,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
handleConfirm = () => {},
|
||||
children,
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
@ -103,7 +103,7 @@ const Modal = ({
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [disableEscapeKey, document]);
|
||||
}, [disableEscapeKey, document, handleConfirm]);
|
||||
|
||||
let classes = 'bruno-modal';
|
||||
if (isClosing) {
|
||||
|
@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.card {
|
||||
background: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
|
||||
|
||||
div.hr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
div.border-top {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,47 @@
|
||||
import { IconLoader2, IconFile } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestIsLoading = ({ item }) => {
|
||||
return <StyledWrapper>
|
||||
<div className='flex flex-col p-4'>
|
||||
<div className='card shadow-sm rounded-md p-4 w-[600px]'>
|
||||
<div>
|
||||
<div className='font-medium flex items-center gap-2 pb-4'>
|
||||
<IconFile size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
File Info
|
||||
</div>
|
||||
<div className='hr'/>
|
||||
|
||||
<div className='flex items-center mt-2'>
|
||||
<span className='w-12 mr-2 text-muted'>Name:</span>
|
||||
<div>
|
||||
{item?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center mt-1'>
|
||||
<span className='w-12 mr-2 text-muted'>Path:</span>
|
||||
<div className='break-all'>
|
||||
{item?.pathname}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center mt-1 pb-4'>
|
||||
<span className='w-12 mr-2 text-muted'>Size:</span>
|
||||
<div>
|
||||
{item?.size?.toFixed?.(2)} MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='hr'/>
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<IconLoader2 className="animate-spin" size={16} strokeWidth={2} />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
}
|
||||
|
||||
export default RequestIsLoading;
|
@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.card {
|
||||
background: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
|
||||
|
||||
div.hr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
div.border-top {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,89 @@
|
||||
import { IconLoader2, IconFile } from '@tabler/icons';
|
||||
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestNotLoaded = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleLoadRequestViaWorker = () => {
|
||||
!item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
const handleLoadRequest = () => {
|
||||
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
return <StyledWrapper>
|
||||
<div className='flex flex-col p-4'>
|
||||
<div className='card shadow-sm rounded-md p-4 w-[600px]'>
|
||||
<div>
|
||||
<div className='font-medium flex items-center gap-2 pb-4'>
|
||||
<IconFile size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
File Info
|
||||
</div>
|
||||
<div className='hr'/>
|
||||
|
||||
<div className='flex items-center mt-2'>
|
||||
<span className='w-12 mr-2 text-muted'>Name:</span>
|
||||
<div>{item?.name}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center mt-1'>
|
||||
<span className='w-12 mr-2 text-muted'>Path:</span>
|
||||
<div className='break-all'>{item?.pathname}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center mt-1 pb-4'>
|
||||
<span className='w-12 mr-2 text-muted'>Size:</span>
|
||||
<div>{item?.size?.toFixed?.(2)} MB</div>
|
||||
</div>
|
||||
|
||||
{!item?.error && (
|
||||
<>
|
||||
<div className='hr'/>
|
||||
<div className='text-muted text-xs mt-4 mb-2'>
|
||||
Due to its large size, this request wasn't loaded automatically.
|
||||
</div>
|
||||
<div className='flex flex-col gap-6 mt-4'>
|
||||
<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}
|
||||
>
|
||||
Load Request
|
||||
</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={handleLoadRequestViaWorker}
|
||||
>
|
||||
Load Request in Background
|
||||
</button>
|
||||
<small className='text-muted mt-1'>
|
||||
Runs in background.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{item?.loading && (
|
||||
<>
|
||||
<div className='hr mt-4'/>
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<IconLoader2 className="animate-spin" size={16} strokeWidth={2} />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
}
|
||||
|
||||
export default RequestNotLoaded;
|
@ -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 CollectionOverview from 'components/CollectionSettings/Overview';
|
||||
import RequestNotLoaded from './RequestNotLoaded';
|
||||
import RequestIsLoading from './RequestIsLoading';
|
||||
|
||||
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 <CollectionOverview 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"
|
||||
|
@ -9,6 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun }
|
||||
import slash from 'utils/common/slash';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
// convert to unix style path
|
||||
@ -106,6 +107,8 @@ export default function RunnerResults({ collection }) {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4">
|
||||
@ -116,7 +119,7 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
|
@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.partial {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.error {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,21 @@
|
||||
import RequestMethod from "../RequestMethod";
|
||||
import { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
|
||||
const CollectionItemIcon = ({ item }) => {
|
||||
if (item?.error) {
|
||||
return <StyledWrapper><IconAlertCircle className="w-fit mr-2 error" size={18} strokeWidth={1.5} /></StyledWrapper>;
|
||||
}
|
||||
|
||||
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;
|
@ -4,6 +4,9 @@ const Wrapper = styled.div`
|
||||
.bruno-modal-content {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.warning {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
@ -7,6 +7,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -32,6 +33,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const flattenedItems = flattenItems(item ? item.items : collection.items);
|
||||
const recursiveRunLength = getRequestsCount(flattenedItems);
|
||||
|
||||
const isFolderLoading = areItemsLoading(item);
|
||||
console.log(item);
|
||||
console.log(isFolderLoading);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Collection Runner" hideFooter={true} handleCancel={onClose}>
|
||||
@ -44,13 +49,12 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
<span className="ml-1 text-xs">({runLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will only run the requests in this folder.</div>
|
||||
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Recursive Run</span>
|
||||
<span className="ml-1 text-xs">({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will run all the requests in this folder and all its subfolders.</div>
|
||||
|
||||
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
||||
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
|
@ -6,12 +6,11 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
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);
|
||||
@ -39,7 +38,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `COLLECTION_ITEM_${collection.uid}`,
|
||||
@ -64,14 +65,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setItemisCollapsed(false);
|
||||
} else {
|
||||
setItemisCollapsed(item.collapsed);
|
||||
}
|
||||
}, [searchText, item]);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@ -227,6 +220,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowInFolder = () => {
|
||||
dispatch(showInFolder(item.pathname)).catch((error) => {
|
||||
console.error('Error opening the folder', error);
|
||||
toast.error('Error opening the folder');
|
||||
});
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
@ -294,12 +294,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>
|
||||
@ -378,6 +378,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
Generate Code
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleShowInFolder();
|
||||
}}
|
||||
>
|
||||
Show in Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
|
@ -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 { collapseCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, 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 CloneCollection from './CloneCollection';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
@ -29,8 +29,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
|
||||
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
@ -52,24 +52,28 @@ const Collection = ({ collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setCollectionIsCollapsed(false);
|
||||
} else {
|
||||
setCollectionIsCollapsed(collection.collapsed);
|
||||
}
|
||||
}, [searchText, collection]);
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
|
||||
|
||||
const iconClassName = classnames({
|
||||
'rotate-90': !collectionIsCollapsed
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
};
|
||||
// Check if the click came from the chevron icon
|
||||
const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
|
||||
|
||||
const handleCollapseCollection = () => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
if (collection.mountStatus === 'unmounted') {
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
}
|
||||
dispatch(collapseCollection(collection.uid));
|
||||
|
||||
// Only open collection settings if not clicking the chevron
|
||||
if(!isChevronClick) {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
@ -78,6 +82,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = menuDropdownTippyRef.current;
|
||||
@ -154,9 +159,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
<IconChevronRight
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
className={iconClassName}
|
||||
className={`chevron-icon ${iconClassName}`}
|
||||
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<div className="ml-1 w-full" id="sidebar-collection-name"
|
||||
onClick={handleCollapseCollection}
|
||||
@ -164,6 +168,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
{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">
|
||||
|
@ -68,7 +68,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
|
@ -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>
|
||||
|
@ -21,8 +21,9 @@ import {
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
|
||||
import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform';
|
||||
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
|
||||
import { callIpc } from 'utils/common/ipc';
|
||||
|
||||
import {
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent,
|
||||
@ -30,6 +31,7 @@ import {
|
||||
removeCollection as _removeCollection,
|
||||
selectEnvironment as _selectEnvironment,
|
||||
sortCollections as _sortCollections,
|
||||
updateCollectionMountStatus,
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
@ -42,7 +44,6 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { name } from 'file-loader';
|
||||
import slash from 'utils/common/slash';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
|
||||
@ -161,7 +162,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 +170,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
pathname: folder.pathname,
|
||||
root: folder.root
|
||||
};
|
||||
console.log(folderData);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
@ -494,7 +493,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
);
|
||||
if (!reqWithSameNameExists) {
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
const fullName = path.join(dirname, filename);
|
||||
const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
|
||||
@ -1193,3 +1192,37 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => {
|
||||
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));
|
||||
return new Promise(async (resolve, reject) => {
|
||||
callIpc('renderer:mount-collection', { collectionUid, collectionPathname, brunoConfig })
|
||||
.then(() => dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounted' })))
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'unmounted' }));
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const showInFolder = (collectionPath) => () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
collapseCollection,
|
||||
collapseAllItemsInCollection,
|
||||
deleteItemInCollection,
|
||||
deleteItemInCollectionByPathname,
|
||||
findCollectionByPathname,
|
||||
@ -32,9 +32,13 @@ export const collectionsSlice = createSlice({
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
collection.settingsSelectedTab = 'headers';
|
||||
collection.settingsSelectedTab = 'overview';
|
||||
collection.folderLevelSettingsSelectedTab = {};
|
||||
|
||||
// Collection mount status is used to track the mount status of the collection
|
||||
// values can be 'unmounted', 'mounting', 'mounted'
|
||||
collection.mountStatus = 'unmounted';
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
@ -44,12 +48,18 @@ export const collectionsSlice = createSlice({
|
||||
collection.importedAt = new Date().getTime();
|
||||
collection.lastAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
collapseAllItemsInCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
state.collections.push(collection);
|
||||
}
|
||||
},
|
||||
updateCollectionMountStatus: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
collection.mountStatus = action.payload.mountStatus;
|
||||
}
|
||||
},
|
||||
setCollectionSecurityConfig: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
@ -358,7 +368,7 @@ export const collectionsSlice = createSlice({
|
||||
collection.items.push(item);
|
||||
}
|
||||
},
|
||||
collectionClicked: (state, action) => {
|
||||
collapseCollection: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload);
|
||||
|
||||
if (collection) {
|
||||
@ -1582,7 +1592,7 @@ export const collectionsSlice = createSlice({
|
||||
name: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: []
|
||||
items: [],
|
||||
};
|
||||
currentSubItems.push(childItem);
|
||||
}
|
||||
@ -1604,6 +1614,10 @@ 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;
|
||||
currentItem.error = file.error;
|
||||
} else {
|
||||
currentSubItems.push({
|
||||
uid: file.data.uid,
|
||||
@ -1613,7 +1627,11 @@ 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,
|
||||
error: file.error
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1890,6 +1908,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
export const {
|
||||
createCollection,
|
||||
updateCollectionMountStatus,
|
||||
setCollectionSecurityConfig,
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
@ -1913,7 +1932,7 @@ export const {
|
||||
saveRequest,
|
||||
deleteRequestDraft,
|
||||
newEphemeralHttpRequest,
|
||||
collectionClicked,
|
||||
collapseCollection,
|
||||
collectionFolderClicked,
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
|
@ -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) {
|
||||
|
@ -114,7 +114,25 @@ const darkTheme = {
|
||||
responseStatus: '#ccc',
|
||||
responseOk: '#8cd656',
|
||||
responseError: '#f06f57',
|
||||
responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
|
||||
responseOverlayBg: 'rgba(30, 30, 30, 0.6)',
|
||||
|
||||
card: {
|
||||
bg: '#252526',
|
||||
border: 'transparent',
|
||||
borderDark: '#8cd656',
|
||||
hr: '#424242'
|
||||
},
|
||||
|
||||
cardTable: {
|
||||
border: '#333',
|
||||
bg: '#252526',
|
||||
table: {
|
||||
thead: {
|
||||
bg: '#3D3D3D',
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
collection: {
|
||||
|
@ -114,7 +114,22 @@ const lightTheme = {
|
||||
responseStatus: 'rgb(117 117 117)',
|
||||
responseOk: '#047857',
|
||||
responseError: 'rgb(185, 28, 28)',
|
||||
responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
|
||||
responseOverlayBg: 'rgba(255, 255, 255, 0.6)',
|
||||
card: {
|
||||
bg: '#fff',
|
||||
border: '#f4f4f4',
|
||||
hr: '#f4f4f4'
|
||||
},
|
||||
cardTable: {
|
||||
border: '#efefef',
|
||||
bg: '#fff',
|
||||
table: {
|
||||
thead: {
|
||||
bg: 'rgb(249, 250, 251)',
|
||||
color: 'rgb(75 85 99)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
collection: {
|
||||
|
@ -34,7 +34,7 @@ export const addDepth = (items = []) => {
|
||||
depth(items, 1);
|
||||
};
|
||||
|
||||
export const collapseCollection = (collection) => {
|
||||
export const collapseAllItemsInCollection = (collection) => {
|
||||
collection.collapsed = true;
|
||||
|
||||
const collapseItem = (items) => {
|
||||
@ -47,7 +47,7 @@ export const collapseCollection = (collection) => {
|
||||
});
|
||||
};
|
||||
|
||||
collapseItem(collection.items, 1);
|
||||
collapseItem(collection.items);
|
||||
};
|
||||
|
||||
export const sortItems = (collection) => {
|
||||
@ -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);
|
||||
|
||||
|
14
packages/bruno-app/src/utils/common/ipc.js
Normal file
14
packages/bruno-app/src/utils/common/ipc.js
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Wrapper for ipcRenderer.invoke that handles error cases
|
||||
* @param {string} channel - The IPC channel name
|
||||
* @param {...any} args - Arguments to pass to the channel
|
||||
* @returns {Promise} - Resolves with the result or rejects with error
|
||||
*/
|
||||
export const callIpc = (channel, ...args) => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) {
|
||||
return Promise.reject(new Error('IPC Renderer not available'));
|
||||
}
|
||||
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
};
|
@ -24,11 +24,25 @@ export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
return relativePath ? relativePath.split(path.sep) : [];
|
||||
};
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
|
||||
return path.dirname(pathname);
|
||||
export const isWindowsPath = (pathname) => {
|
||||
|
||||
if (!isWindowsOS()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for Windows drive letter format (e.g., "C:\")
|
||||
const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname);
|
||||
|
||||
// Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path
|
||||
const isUNCPath = pathname.startsWith('\\\\');
|
||||
|
||||
return hasDriveLetter || isUNCPath;
|
||||
};
|
||||
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
|
@ -52,7 +52,7 @@
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
@ -66,7 +66,6 @@
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection } = require('../utils/collection');
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
@ -18,6 +19,7 @@ const printRunSummary = (results) => {
|
||||
let totalRequests = 0;
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let skippedRequests = 0;
|
||||
let totalAssertions = 0;
|
||||
let passedAssertions = 0;
|
||||
let failedAssertions = 0;
|
||||
@ -49,7 +51,10 @@ const printRunSummary = (results) => {
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
if (!hasAnyTestsOrAssertions && result.error) {
|
||||
if (!hasAnyTestsOrAssertions && result.skipped) {
|
||||
skippedRequests += 1;
|
||||
}
|
||||
else if (!hasAnyTestsOrAssertions && result.error) {
|
||||
failedRequests += 1;
|
||||
} else {
|
||||
passedRequests += 1;
|
||||
@ -62,6 +67,9 @@ const printRunSummary = (results) => {
|
||||
if (failedRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
|
||||
}
|
||||
if (skippedRequests > 0) {
|
||||
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
|
||||
}
|
||||
requestSummary += `, ${totalRequests} total`;
|
||||
|
||||
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
|
||||
@ -84,6 +92,7 @@ const printRunSummary = (results) => {
|
||||
totalRequests,
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
skippedRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
@ -144,7 +153,7 @@ const createCollectionFromPath = (collectionPath) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
return currentDirItems
|
||||
return currentDirItems;
|
||||
};
|
||||
collection.items = traverse(collectionPath);
|
||||
return collection;
|
||||
@ -634,6 +643,34 @@ const handler = async function (argv) {
|
||||
}
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
|
||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let itemPathname = path.join(collectionPath, relativeItemPathname);
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
|
||||
if (bruJson) {
|
||||
const res = await runSingleRequest(
|
||||
itemPathname,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
runtimeVariables,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
resolve(res?.response);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
@ -651,7 +688,8 @@ const handler = async function (argv) {
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
|
||||
results.push({
|
||||
@ -701,6 +739,11 @@ const handler = async function (argv) {
|
||||
|
||||
// determine next request
|
||||
const nextRequestName = result?.nextRequestName;
|
||||
|
||||
if (result?.shouldStopRunnerExecution) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
|
@ -36,7 +36,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
};
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
if (collectionAuth && request.auth.mode === 'inherit') {
|
||||
if (collectionAuth && request.auth?.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
username: get(collectionAuth, 'basic.username'),
|
||||
@ -47,9 +47,27 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
if (collectionAuth.mode === 'bearer') {
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'apikey') {
|
||||
if (collectionAuth.apikey?.placement === 'header') {
|
||||
axiosRequest.headers[collectionAuth.apikey?.key] = collectionAuth.apikey?.value;
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
if (collectionAuth.apikey?.placement === 'queryparams') {
|
||||
if (axiosRequest.url && collectionAuth.apikey?.key) {
|
||||
try {
|
||||
const urlObj = new URL(request.url);
|
||||
urlObj.searchParams.set(collectionAuth.apikey?.key, collectionAuth.apikey?.value);
|
||||
axiosRequest.url = urlObj.toString();
|
||||
} catch (error) {
|
||||
console.error('Invalid URL:', request.url, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
if (request.auth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
|
@ -40,11 +40,13 @@ const runSingleRequest = async function (
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
) {
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
let shouldStopRunnerExecution = false;
|
||||
let item = {
|
||||
pathname: path.join(collectionPath, filename),
|
||||
...bruJson
|
||||
@ -68,11 +70,41 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (result?.skipRequest) {
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via pre-request script',
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: 'Request has been skipped from pre-request script',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@ -255,7 +287,7 @@ const runSingleRequest = async function (
|
||||
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance)
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
@ -323,7 +355,8 @@ const runSingleRequest = async function (
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
nextRequestName: nextRequestName
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -363,11 +396,16 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
}
|
||||
|
||||
// run assertions
|
||||
@ -408,13 +446,18 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (testResults?.length) {
|
||||
@ -447,7 +490,8 @@ const runSingleRequest = async function (
|
||||
error: null,
|
||||
assertionResults,
|
||||
testResults,
|
||||
nextRequestName: nextRequestName
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
|
@ -204,5 +204,6 @@ module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
findItemInCollection,
|
||||
getTreePathFromCollectionToItem
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
const { describe, it, expect, beforeEach } = require('@jest/globals');
|
||||
const prepareRequest = require('../../src/runner/prepare-request');
|
||||
|
||||
describe('prepare-request: prepareRequest', () => {
|
||||
@ -22,4 +21,144 @@ describe('prepare-request: prepareRequest', () => {
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Properly maps inherited auth from collectionRoot', () => {
|
||||
// Initialize Test Fixtures
|
||||
let collection, item;
|
||||
|
||||
beforeEach(() => {
|
||||
collection = {
|
||||
name: 'Test Collection',
|
||||
root: {
|
||||
request: {
|
||||
auth: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
item = {
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
url: 'https://usebruno.com',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("Pre Request")',
|
||||
res: 'console.log("Post Response")'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('API Key Authentication', () => {
|
||||
it('If collection auth is apikey in header', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "header"
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
|
||||
});
|
||||
|
||||
it('If collection auth is apikey in header and request has existing headers', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "header"
|
||||
}
|
||||
};
|
||||
|
||||
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
|
||||
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
|
||||
});
|
||||
|
||||
it('If collection auth is apikey in query parameters', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "queryparams"
|
||||
}
|
||||
};
|
||||
|
||||
const urlObj = new URL(item.request.url);
|
||||
urlObj.searchParams.set(collection.root.request.auth.apikey.key, collection.root.request.auth.apikey.value);
|
||||
|
||||
const expected = urlObj.toString();
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.url).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic Authentication', () => {
|
||||
it('If collection auth is basic auth', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
const expected = { username: 'testUser', password: 'testPass123' };
|
||||
expect(result.auth).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bearer Token Authentication', () => {
|
||||
it('If collection auth is bearer token', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'bearer',
|
||||
bearer: {
|
||||
token: 'token'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
|
||||
});
|
||||
|
||||
it('If collection auth is bearer token and request has existing headers', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'bearer',
|
||||
bearer: {
|
||||
token: 'token'
|
||||
}
|
||||
};
|
||||
|
||||
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
|
||||
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('No Authentication', () => {
|
||||
it('If request does not have auth configured', () => {
|
||||
delete item.request.auth;
|
||||
let result;
|
||||
expect(() => {
|
||||
result = prepareRequest(item, collection);
|
||||
}).not.toThrow();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "electron .",
|
||||
"debug": "electron . --inspect=9229",
|
||||
"dist:mac": "electron-builder --mac --config electron-builder-config.js",
|
||||
"dist:win": "electron-builder --win --config electron-builder-config.js",
|
||||
"dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js",
|
||||
@ -33,7 +34,7 @@
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
|
@ -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, getCollectionStats } = 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) {
|
||||
@ -70,6 +70,10 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => {
|
||||
brunoConfig.ignore = ['node_modules', '.git'];
|
||||
}
|
||||
|
||||
const { size, filesCount } = await getCollectionStats(collectionPath);
|
||||
brunoConfig.size = size;
|
||||
brunoConfig.filesCount = filesCount;
|
||||
|
||||
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig);
|
||||
} catch (err) {
|
||||
|
@ -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, bruToJsonViaWorker ,collectionBruToJson } = 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 { parseBruFileMeta, 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,7 +160,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
}
|
||||
};
|
||||
|
||||
const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => {
|
||||
console.log(`watcher add: ${pathname}`);
|
||||
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
@ -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,15 +254,67 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const fileStats = fs.statSync(pathname);
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = bruToJson(bruContent);
|
||||
|
||||
// If worker thread is not used, we can directly parse the file
|
||||
if (!useWorkerThread) {
|
||||
try {
|
||||
file.data = await bruToJson(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 (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// we need to send a partial file info to the UI
|
||||
// so that the UI can display the file in the collection tree
|
||||
file.data = {
|
||||
name: path.basename(pathname),
|
||||
type: 'http-request'
|
||||
};
|
||||
|
||||
const metaJson = await bruToJson(parseBruFileMeta(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) {
|
||||
// This is to update the loading indicator in the UI
|
||||
file.data = metaJson;
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
|
||||
// This is to update the file info in the UI
|
||||
file.data = await bruToJsonViaWorker(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = false;
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
}
|
||||
} catch(error) {
|
||||
file.data = {
|
||||
name: path.basename(pathname),
|
||||
type: 'http-request'
|
||||
};
|
||||
file.error = {
|
||||
message: error?.message
|
||||
};
|
||||
file.partial = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -357,7 +389,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 +410,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);
|
||||
@ -424,10 +456,10 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||
};
|
||||
|
||||
const onWatcherSetupComplete = (win, collectionPath) => {
|
||||
const onWatcherSetupComplete = (win, watchPath) => {
|
||||
const UiStateSnapshotStore = new UiStateSnapshot();
|
||||
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
|
||||
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath);
|
||||
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath);
|
||||
win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState);
|
||||
};
|
||||
|
||||
@ -436,7 +468,7 @@ class Watcher {
|
||||
this.watchers = {};
|
||||
}
|
||||
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
@ -467,7 +499,7 @@ class Watcher {
|
||||
let startedNewWatcher = false;
|
||||
watcher
|
||||
.on('ready', () => onWatcherSetupComplete(win, watchPath))
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread))
|
||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
@ -488,7 +520,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, useWorkerThread);
|
||||
} else {
|
||||
console.error(`An error occurred in the watcher for: ${watchPath}`, error);
|
||||
}
|
||||
|
@ -7,10 +7,13 @@ const {
|
||||
collectionBruToJson: _collectionBruToJson,
|
||||
jsonToCollectionBru: _jsonToCollectionBru
|
||||
} = require('@usebruno/lang');
|
||||
const BruParserWorker = require('./workers');
|
||||
|
||||
const collectionBruToJson = (bru) => {
|
||||
const bruParserWorker = new BruParserWorker();
|
||||
|
||||
const collectionBruToJson = async (data, parsed = false) => {
|
||||
try {
|
||||
const json = _collectionBruToJson(bru);
|
||||
const json = parsed ? data : _collectionBruToJson(data);
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
@ -38,7 +41,7 @@ const collectionBruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
const jsonToCollectionBru = (json, isFolder) => {
|
||||
const jsonToCollectionBru = async (json, isFolder) => {
|
||||
try {
|
||||
const collectionBruJson = {
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
@ -73,7 +76,7 @@ const jsonToCollectionBru = (json, isFolder) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bruToEnvJson = (bru) => {
|
||||
const bruToEnvJson = async (bru) => {
|
||||
try {
|
||||
const json = bruToEnvJsonV2(bru);
|
||||
|
||||
@ -90,7 +93,7 @@ const bruToEnvJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
const envJsonToBru = (json) => {
|
||||
const envJsonToBru = async (json) => {
|
||||
try {
|
||||
const bru = envJsonToBruV2(json);
|
||||
return bru;
|
||||
@ -105,12 +108,12 @@ const envJsonToBru = (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.
|
||||
* @param {string} data The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
const bruToJson = (bru) => {
|
||||
const bruToJson = (data, parsed = false) => {
|
||||
try {
|
||||
const json = bruToJsonV2(bru);
|
||||
const json = parsed ? data : bruToJsonV2(data);
|
||||
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
@ -149,6 +152,16 @@ const bruToJson = (bru) => {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
const bruToJsonViaWorker = async (data) => {
|
||||
try {
|
||||
const json = await bruParserWorker?.bruToJson(data);
|
||||
return bruToJson(json, true);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a JSON to BRU file.
|
||||
*
|
||||
@ -158,7 +171,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,14 +208,59 @@ const jsonToBru = (json) => {
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
};
|
||||
|
||||
return jsonToBruV2(bruJson);
|
||||
const bru = jsonToBruV2(bruJson);
|
||||
return bru;
|
||||
};
|
||||
|
||||
const jsonToBruViaWorker = async (json) => {
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'seq');
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
params: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
};
|
||||
|
||||
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
||||
return bru;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToJsonViaWorker,
|
||||
jsonToBru,
|
||||
bruToEnvJson,
|
||||
envJsonToBru,
|
||||
collectionBruToJson,
|
||||
jsonToCollectionBru
|
||||
jsonToCollectionBru,
|
||||
jsonToBruViaWorker
|
||||
};
|
||||
|
57
packages/bruno-electron/src/bru/workers/index.js
Normal file
57
packages/bruno-electron/src/bru/workers/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lanes are used to determine which worker queue to use based on the size of the data.
|
||||
*
|
||||
* The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB).
|
||||
* This helps with parsing performance.
|
||||
*/
|
||||
const LANES = [{
|
||||
maxSize: 0.1
|
||||
},{
|
||||
maxSize: 100
|
||||
}];
|
||||
|
||||
class BruParserWorker {
|
||||
constructor() {
|
||||
this.workerQueues = LANES?.map(lane => ({
|
||||
maxSize: lane?.maxSize,
|
||||
workerQueue: new WorkerQueue()
|
||||
}));
|
||||
}
|
||||
|
||||
getWorkerQueue(size) {
|
||||
// Find the first queue that can handle the given size
|
||||
// or fallback to the last queue for largest files
|
||||
const queueForSize = this.workerQueues.find((queue) =>
|
||||
queue.maxSize >= size
|
||||
);
|
||||
|
||||
return queueForSize?.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` });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BruParserWorker;
|
@ -0,0 +1,14 @@
|
||||
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);
|
||||
parentPort.postMessage({ error: error?.message });
|
||||
}
|
@ -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);
|
||||
parentPort.postMessage({ error: error?.message });
|
||||
}
|
@ -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, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
|
||||
|
||||
const {
|
||||
isValidPathname,
|
||||
@ -24,6 +24,8 @@ const {
|
||||
isWindowsOS,
|
||||
isValidFilename,
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
@ -32,11 +34,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 { parseBruFileMeta, 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);
|
||||
|
||||
@ -97,6 +105,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const content = await stringifyJson(brunoConfig);
|
||||
await writeFile(path.join(dirPath, 'bruno.json'), content);
|
||||
|
||||
const { size, filesCount } = await getCollectionStats(dirPath);
|
||||
brunoConfig.size = size;
|
||||
brunoConfig.filesCount = filesCount;
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig);
|
||||
} catch (error) {
|
||||
@ -126,9 +138,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 +159,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
fs.copyFileSync(sourceFilePath, newFilePath);
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, json);
|
||||
const { size, filesCount } = await getCollectionStats(dirPath);
|
||||
brunoConfig.size = size;
|
||||
brunoConfig.filesCount = filesCount;
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
}
|
||||
);
|
||||
@ -184,7 +200,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
name: folderName
|
||||
};
|
||||
|
||||
const content = jsonToCollectionBru(
|
||||
const content = await jsonToCollectionBru(
|
||||
folderRoot,
|
||||
true // isFolder
|
||||
);
|
||||
@ -197,7 +213,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 +229,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 jsonToBruViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@ -227,7 +243,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = jsonToBru(request);
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@ -245,7 +261,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = jsonToBru(request);
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -275,7 +291,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 +316,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 +428,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 bruToJsonViaWorker(data);
|
||||
jsonData.name = newName;
|
||||
moveRequestUid(oldPath, newPath);
|
||||
|
||||
const content = jsonToBru(jsonData);
|
||||
const content = await jsonToBruViaWorker(jsonData);
|
||||
await fs.promises.unlink(oldPath);
|
||||
await writeFile(newPath, content);
|
||||
|
||||
@ -516,9 +532,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 jsonToBruViaWorker(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
@ -529,7 +545,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 +570,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 +595,19 @@ 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);
|
||||
|
||||
const { size, filesCount } = await getCollectionStats(collectionPath);
|
||||
brunoConfig.size = size;
|
||||
brunoConfig.filesCount = filesCount;
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);
|
||||
|
||||
@ -609,9 +629,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 jsonToBruViaWorker(item);
|
||||
const filePath = path.join(currentPath, `${item.name}.bru`);
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
@ -621,7 +641,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 +659,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 +675,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 bruToJsonViaWorker(bru);
|
||||
|
||||
if (jsonData.seq !== item.seq) {
|
||||
jsonData.seq = item.seq;
|
||||
const content = jsonToBru(jsonData);
|
||||
const content = await jsonToBruViaWorker(jsonData);
|
||||
await writeFile(item.pathname, content);
|
||||
}
|
||||
}
|
||||
@ -776,6 +796,131 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request-via-worker', 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(parseBruFileMeta(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);
|
||||
file.data = await bruToJsonViaWorker(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(parseBruFileMeta(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', 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(parseBruFileMeta(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);
|
||||
file.data = 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(parseBruFileMeta(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:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
|
||||
const {
|
||||
size,
|
||||
filesCount,
|
||||
maxFileSize
|
||||
} = await getCollectionStats(collectionPathname);
|
||||
|
||||
const shouldLoadCollectionAsync =
|
||||
(size > MAX_COLLECTION_SIZE_IN_MB) ||
|
||||
(filesCount > MAX_COLLECTION_FILES_COUNT) ||
|
||||
(maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB);
|
||||
|
||||
watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:show-in-folder', async (event, filePath) => {
|
||||
try {
|
||||
if (!filePath) {
|
||||
throw new Error('File path is required');
|
||||
}
|
||||
shell.showItemInFolder(filePath);
|
||||
} catch (error) {
|
||||
console.error('Error in show-in-folder: ', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
@ -790,8 +935,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);
|
||||
});
|
||||
|
@ -274,11 +274,10 @@ const configureRequest = async (
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance)
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
@ -403,7 +402,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -437,6 +436,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@ -470,7 +471,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -507,6 +508,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (result?.error) {
|
||||
mainWindow.webContents.send('main:display-error', result.error);
|
||||
}
|
||||
|
||||
collection.globalEnvironmentVariables = result.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// run post-response script
|
||||
@ -537,11 +540,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
}
|
||||
return scriptResult;
|
||||
};
|
||||
|
||||
const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => {
|
||||
const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const cancelTokenUid = uuid();
|
||||
@ -553,9 +558,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
@ -570,11 +575,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const request = prepareRequest(item, collection);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
@ -589,7 +591,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -674,7 +676,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -758,7 +760,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
return await runRequest({ item, collection, environment, runtimeVariables });
|
||||
const collectionUid = collection.uid;
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
|
||||
});
|
||||
|
||||
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
|
||||
@ -782,7 +787,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -818,7 +823,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -888,7 +893,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -912,7 +917,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -949,7 +954,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
let stopRunnerExecution = false;
|
||||
|
||||
const abortController = new AbortController();
|
||||
@ -961,9 +967,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
@ -983,7 +989,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
try {
|
||||
const envVars = getEnvVars(environment);
|
||||
let folderRequests = [];
|
||||
|
||||
if (recursive) {
|
||||
@ -1035,7 +1040,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
const requestUid = uuid();
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
|
||||
try {
|
||||
const preRequestScriptResult = await runPreRequest(
|
||||
@ -1043,7 +1047,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@ -1181,7 +1185,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collectionRoot,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
|
@ -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');
|
||||
|
||||
@ -7,7 +11,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
if (header?.name?.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
@ -19,14 +23,14 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let _headers = get(i, 'root.request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -203,6 +207,51 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const parseBruFileMeta = (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,
|
||||
parseBruFileMeta,
|
||||
hydrateRequestWithUuid
|
||||
};
|
@ -211,6 +211,50 @@ 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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidPathname,
|
||||
exists,
|
||||
@ -235,5 +279,7 @@ module.exports = {
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename,
|
||||
hasSubDirectories
|
||||
hasSubDirectories,
|
||||
getCollectionStats,
|
||||
sizeInMB
|
||||
};
|
||||
|
60
packages/bruno-electron/src/workers/index.js
Normal file
60
packages/bruno-electron/src/workers/index.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
class WorkerQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
async enqueue(task) {
|
||||
const { priority, scriptPath, data } = task;
|
||||
|
||||
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) => {
|
||||
if (data?.error) {
|
||||
reject(new Error(data?.error));
|
||||
}
|
||||
resolve(data);
|
||||
worker.terminate();
|
||||
});
|
||||
worker.on('error', (error) => {
|
||||
reject(error);
|
||||
worker.terminate();
|
||||
});
|
||||
worker.on('exit', (code) => {
|
||||
reject(new Error(`stopped with ${code} exit code`));
|
||||
worker.terminate();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkerQueue;
|
121
packages/bruno-electron/tests/utils/collection.spec.js
Normal file
121
packages/bruno-electron/tests/utils/collection.spec.js
Normal file
@ -0,0 +1,121 @@
|
||||
const { parseBruFileMeta } = require("../../src/utils/collection");
|
||||
|
||||
describe('parseBruFileMeta', () => {
|
||||
test('parses valid meta block correctly', () => {
|
||||
const data = `meta {
|
||||
name: 0.2_mb
|
||||
type: http
|
||||
seq: 1
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
name: '0.2_mb',
|
||||
type: 'http',
|
||||
seq: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns undefined for missing meta block', () => {
|
||||
const data = `someOtherBlock {
|
||||
key: value
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('handles empty meta block gracefully', () => {
|
||||
const data = `meta {}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({ meta: {} });
|
||||
});
|
||||
|
||||
test('ignores invalid lines in meta block', () => {
|
||||
const data = `meta {
|
||||
name: 0.2_mb
|
||||
invalidLine
|
||||
seq: 1
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
name: '0.2_mb',
|
||||
seq: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('handles unexpected input gracefully', () => {
|
||||
const data = null;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('handles missing colon gracefully', () => {
|
||||
const data = `meta {
|
||||
name 0.2_mb
|
||||
seq: 1
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
seq: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('parses numeric values correctly', () => {
|
||||
const data = `meta {
|
||||
numValue: 1234
|
||||
floatValue: 12.34
|
||||
strValue: some_text
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
numValue: 1234,
|
||||
floatValue: 12.34,
|
||||
strValue: 'some_text',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('handles syntax error in meta block 1', () => {
|
||||
const data = `meta
|
||||
name: 0.2_mb
|
||||
type: http
|
||||
seq: 1
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('handles syntax error in meta block 2', () => {
|
||||
const data = `meta {
|
||||
name: 0.2_mb
|
||||
type: http
|
||||
seq: 1
|
||||
`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
@ -21,7 +21,7 @@
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
|
@ -1,3 +1,5 @@
|
||||
const { get } = require('@usebruno/query');
|
||||
|
||||
class BrunoResponse {
|
||||
constructor(res) {
|
||||
this.res = res;
|
||||
@ -6,6 +8,13 @@ class BrunoResponse {
|
||||
this.headers = res ? res.headers : null;
|
||||
this.body = res ? res.data : null;
|
||||
this.responseTime = res ? res.responseTime : null;
|
||||
|
||||
// Make the instance callable
|
||||
const callable = (...args) => get(this.body, ...args);
|
||||
Object.setPrototypeOf(callable, this.constructor.prototype);
|
||||
Object.assign(callable, this);
|
||||
|
||||
return callable;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
|
15
packages/bruno-tests/collection/ping-another-one.bru
Normal file
15
packages/bruno-tests/collection/ping-another-one.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: ping-another-one
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
throw new Error('this should not execute in a collection run');
|
||||
}
|
@ -9,3 +9,7 @@ get {
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.runner.stopExecution();
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
meta {
|
||||
name: runRequest-1
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:text {
|
||||
bruno
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
// reset values
|
||||
bru.setVar('run-request-runtime-var', null);
|
||||
bru.setEnvVar('run-request-env-var', null);
|
||||
bru.setGlobalEnvVar('run-request-global-env-var', null);
|
||||
|
||||
// the above vars will be set in the below request
|
||||
const resp = await bru.runRequest('scripting/api/bru/runRequest-2');
|
||||
|
||||
bru.setVar('run-request-resp', {
|
||||
data: resp?.data,
|
||||
statusText: resp?.statusText,
|
||||
status: resp?.status
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should get runtime var set in runRequest-2", function() {
|
||||
const val = bru.getVar("run-request-runtime-var");
|
||||
expect(val).to.equal("run-request-runtime-var-value");
|
||||
});
|
||||
|
||||
test("should get env var set in runRequest-2", function() {
|
||||
const val = bru.getEnvVar("run-request-env-var");
|
||||
expect(val).to.equal("run-request-env-var-value");
|
||||
});
|
||||
|
||||
test("should get global env var set in runRequest-2", function() {
|
||||
const val = bru.getGlobalEnvVar("run-request-global-env-var");
|
||||
const executionMode = req.getExecutionMode();
|
||||
if (executionMode == 'runner') {
|
||||
expect(val).to.equal("run-request-global-env-var-value");
|
||||
}
|
||||
});
|
||||
|
||||
test("should get response of runRequest-2", function() {
|
||||
const val = bru.getVar('run-request-resp');
|
||||
expect(JSON.stringify(val)).to.equal(JSON.stringify({
|
||||
"data": "bruno",
|
||||
"statusText": "OK",
|
||||
"status": 200
|
||||
}));
|
||||
});
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
meta {
|
||||
name: runRequest-2
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:text {
|
||||
bruno
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value');
|
||||
bru.setEnvVar('run-request-env-var', 'run-request-env-var-value');
|
||||
bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value');
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
meta {
|
||||
name: runRequest
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
foo: bar
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: asd
|
||||
password: j
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token:
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "bruno"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar("runRequest-ping-res-1", null);
|
||||
bru.setVar("runRequest-ping-res-2", null);
|
||||
bru.setVar("runRequest-ping-res-3", null);
|
||||
|
||||
let pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-1', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
let pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-2', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
const pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-3', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
|
||||
test("should run request and return valid response in pre-request script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-1');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
test("should run request and return valid response in post-response script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-2');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
test("should run request and return valid response in tests script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-3');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: 1
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('bru-runner-req', 1);
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setVar('bru.runner.skipRequest', true);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: 2
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.runner.skipRequest();
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setVar('bru.runner.skipRequest', false);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: 3
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection_oauth2",
|
||||
"name": "OAuth2 Demo",
|
||||
"type": "collection",
|
||||
"scripts": {
|
||||
"moduleWhitelist": ["crypto"],
|
||||
"moduleWhitelist": [
|
||||
"crypto"
|
||||
],
|
||||
"filesystemAccess": {
|
||||
"allow": true
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/usebruno/bruno-testbench#readme",
|
||||
"dependencies": {
|
||||
"axios": "1.7.5",
|
||||
"axios": "1.7.7",
|
||||
"body-parser": "1.20.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
|
Loading…
Reference in New Issue
Block a user