forked from extern/bruno
feat: collection runner
This commit is contained in:
parent
c5b509115a
commit
58bc247c53
@ -12,6 +12,7 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|||||||
import RequestNotFound from './RequestNotFound';
|
import RequestNotFound from './RequestNotFound';
|
||||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||||
|
import RunnerResults from 'components/RunnerResults';
|
||||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -112,6 +113,11 @@ const RequestTabPanel = () => {
|
|||||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showRunner = collection.showRunner;
|
||||||
|
if(showRunner) {
|
||||||
|
return <RunnerResults collection={collection}/>;
|
||||||
|
}
|
||||||
|
|
||||||
const item = findItemInCollection(collection, activeTabUid);
|
const item = findItemInCollection(collection, activeTabUid);
|
||||||
if (!item || !item.uid) {
|
if (!item || !item.uid) {
|
||||||
return <RequestNotFound itemUid={activeTabUid} />;
|
return <RequestNotFound itemUid={activeTabUid} />;
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconFiles } from '@tabler/icons';
|
import { IconFiles, IconRun } from '@tabler/icons';
|
||||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||||
import VariablesView from 'components/VariablesView';
|
import VariablesView from 'components/VariablesView';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const CollectionToolBar = ({ collection }) => {
|
const CollectionToolBar = ({ collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleRun = () => {
|
||||||
|
dispatch(toggleRunnerView({
|
||||||
|
collectionUid: collection.uid
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="flex items-center p-2">
|
<div className="flex items-center p-2">
|
||||||
@ -13,6 +23,9 @@ const CollectionToolBar = ({ collection }) => {
|
|||||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 items-center justify-end">
|
<div className="flex flex-1 items-center justify-end">
|
||||||
|
<span className="mr-2">
|
||||||
|
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||||
|
</span>
|
||||||
<VariablesView collection={collection}/>
|
<VariablesView collection={collection}/>
|
||||||
<EnvironmentSelector collection={collection} />
|
<EnvironmentSelector collection={collection} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
|
|||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import NewRequest from 'components/Sidebar/NewRequest';
|
import NewRequest from 'components/Sidebar/NewRequest';
|
||||||
@ -76,6 +76,8 @@ const RequestTabs = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showRunner = activeCollection && activeCollection.showRunner;
|
||||||
|
|
||||||
// Todo: Must support ephermal requests
|
// Todo: Must support ephermal requests
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={getRootClassname()}>
|
<StyledWrapper className={getRootClassname()}>
|
||||||
@ -83,6 +85,7 @@ const RequestTabs = () => {
|
|||||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||||
<>
|
<>
|
||||||
<CollectionToolBar collection={activeCollection} />
|
<CollectionToolBar collection={activeCollection} />
|
||||||
|
{!showRunner ? (
|
||||||
<div className="flex items-center pl-4">
|
<div className="flex items-center pl-4">
|
||||||
<ul role="tablist">
|
<ul role="tablist">
|
||||||
{showChevrons ? (
|
{showChevrons ? (
|
||||||
@ -136,6 +139,7 @@ const RequestTabs = () => {
|
|||||||
</li> */}
|
</li> */}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
@ -6,13 +6,18 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const QueryResult = ({ item, collection, value, width }) => {
|
const QueryResult = ({ item, collection, value, width, disableRunEventListener }) => {
|
||||||
const {
|
const {
|
||||||
storedTheme
|
storedTheme
|
||||||
} = useTheme();
|
} = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
const onRun = () => {
|
||||||
|
if(disableRunEventListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(sendRequest(item, collection.uid));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const TestResultsLabel = ({ results }) => {
|
||||||
|
if(!results || !results.length) {
|
||||||
|
return 'Tests';
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfTests = results.length;
|
||||||
|
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div>Tests</div>
|
||||||
|
{numberOfFailedTests ? (
|
||||||
|
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||||
|
{numberOfFailedTests}
|
||||||
|
</sup>
|
||||||
|
) : (
|
||||||
|
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||||
|
{numberOfTests}
|
||||||
|
</sup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestResultsLabel;
|
@ -3,12 +3,13 @@ import forOwn from 'lodash/forOwn';
|
|||||||
import { safeStringifyJSON } from 'utils/common';
|
import { safeStringifyJSON } from 'utils/common';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const Timeline = ({ item }) => {
|
const Timeline = ({ request, response}) => {
|
||||||
const request = item.requestSent || {};
|
|
||||||
const response = item.response || {};
|
|
||||||
const requestHeaders = [];
|
const requestHeaders = [];
|
||||||
const responseHeaders = response.headers || [];
|
const responseHeaders = response.headers || [];
|
||||||
|
|
||||||
|
request = request || {};
|
||||||
|
response = response || {};
|
||||||
|
|
||||||
forOwn(request.headers, (value, key) => {
|
forOwn(request.headers, (value, key) => {
|
||||||
requestHeaders.push({
|
requestHeaders.push({
|
||||||
name: key,
|
name: key,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { safeStringifyJSON } from 'utils/common';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import QueryResult from './QueryResult';
|
import QueryResult from './QueryResult';
|
||||||
@ -12,32 +13,9 @@ import ResponseTime from './ResponseTime';
|
|||||||
import ResponseSize from './ResponseSize';
|
import ResponseSize from './ResponseSize';
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
import TestResults from './TestResults';
|
import TestResults from './TestResults';
|
||||||
|
import TestResultsLabel from './TestResultsLabel';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const TestResultsLabel = ({ results }) => {
|
|
||||||
if(!results || !results.length) {
|
|
||||||
return 'Tests';
|
|
||||||
}
|
|
||||||
|
|
||||||
const numberOfTests = results.length;
|
|
||||||
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<div>Tests</div>
|
|
||||||
{numberOfFailedTests ? (
|
|
||||||
<sup className='sups some-tests-failed ml-1 font-medium'>
|
|
||||||
{numberOfFailedTests}
|
|
||||||
</sup>
|
|
||||||
) : (
|
|
||||||
<sup className='sups all-tests-passed ml-1 font-medium'>
|
|
||||||
{numberOfTests}
|
|
||||||
</sup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const tabs = useSelector((state) => state.tabs.tabs);
|
const tabs = useSelector((state) => state.tabs.tabs);
|
||||||
@ -62,14 +40,14 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
item={item}
|
item={item}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
width={rightPaneWidth}
|
width={rightPaneWidth}
|
||||||
value={response.data ? JSON.stringify(response.data, null, 2) : ''
|
value={response.data ? safeStringifyJSON(response.data, true) : ''}
|
||||||
} />;
|
/>;
|
||||||
}
|
}
|
||||||
case 'headers': {
|
case 'headers': {
|
||||||
return <ResponseHeaders headers={response.headers} />;
|
return <ResponseHeaders headers={response.headers} />;
|
||||||
}
|
}
|
||||||
case 'timeline': {
|
case 'timeline': {
|
||||||
return <Timeline item={item} />;
|
return <Timeline request={item.requestSent} response={item.response}/>;
|
||||||
}
|
}
|
||||||
case 'tests': {
|
case 'tests': {
|
||||||
return <TestResults results={item.testResults} />;
|
return <TestResults results={item.testResults} />;
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
div.tabs {
|
||||||
|
div.tab {
|
||||||
|
padding: 6px 0px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: solid 2px transparent;
|
||||||
|
margin-right: 1.25rem;
|
||||||
|
color: var(--color-tab-inactive);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:active,
|
||||||
|
&:focus-within,
|
||||||
|
&:focus-visible,
|
||||||
|
&:target {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||||
|
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.some-tests-failed {
|
||||||
|
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-tests-passed {
|
||||||
|
color: ${(props) => props.theme.colors.text.green} !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { safeStringifyJSON } from 'utils/common';
|
||||||
|
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||||
|
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||||
|
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||||
|
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||||
|
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||||
|
import Timeline from 'components/ResponsePane/Timeline';
|
||||||
|
import TestResults from 'components/ResponsePane/TestResults';
|
||||||
|
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||||
|
const [selectedTab, setSelectedTab] = useState('response');
|
||||||
|
|
||||||
|
const {
|
||||||
|
requestSent,
|
||||||
|
responseReceived,
|
||||||
|
testResults
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
const headers = get(item, 'responseReceived.headers', {});
|
||||||
|
const status = get(item, 'responseReceived.status', 0);
|
||||||
|
const size = get(item, 'responseReceived.size', 0);
|
||||||
|
const duration = get(item, 'responseReceived.duration', 0);
|
||||||
|
|
||||||
|
const selectTab = (tab) => setSelectedTab(tab);
|
||||||
|
|
||||||
|
const getTabPanel = (tab) => {
|
||||||
|
switch (tab) {
|
||||||
|
case 'response': {
|
||||||
|
return <QueryResult
|
||||||
|
item={item}
|
||||||
|
collection={collection}
|
||||||
|
width={rightPaneWidth}
|
||||||
|
disableRunEventListener={true}
|
||||||
|
value={(responseReceived && responseReceived.data) ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
case 'headers': {
|
||||||
|
return <ResponseHeaders headers={headers} />;
|
||||||
|
}
|
||||||
|
case 'timeline': {
|
||||||
|
return <Timeline request={requestSent} response={responseReceived} />;
|
||||||
|
}
|
||||||
|
case 'tests': {
|
||||||
|
return <TestResults results={testResults} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return <div>404 | Not found</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabClassname = (tabName) => {
|
||||||
|
return classnames(`tab select-none ${tabName}`, {
|
||||||
|
active: tabName === selectedTab
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="flex flex-col h-full relative">
|
||||||
|
<div className="flex items-center px-3 tabs" role="tablist">
|
||||||
|
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||||
|
Response
|
||||||
|
</div>
|
||||||
|
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||||
|
Headers
|
||||||
|
</div>
|
||||||
|
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||||
|
Timeline
|
||||||
|
</div>
|
||||||
|
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||||
|
<TestResultsLabel results={testResults} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow justify-end items-center">
|
||||||
|
<StatusCode status={status} />
|
||||||
|
<ResponseTime duration={duration} />
|
||||||
|
<ResponseSize size={size} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResponsePane;
|
@ -0,0 +1,24 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
.item-path {
|
||||||
|
.link {
|
||||||
|
color: ${(props) => props.theme.textLink};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* test results */
|
||||||
|
.test-success {
|
||||||
|
color: ${(props) => props.theme.colors.text.green};
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-failure {
|
||||||
|
color: ${(props) => props.theme.colors.text.danger};
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: ${(props) => props.theme.colors.text.muted};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
127
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
127
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import path from 'path';
|
||||||
|
import { get, each, cloneDeep } from 'lodash';
|
||||||
|
import { findItemInCollection } from 'utils/collections';
|
||||||
|
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||||
|
import ResponsePane from './ResponsePane';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const getRelativePath = (fullPath, pathname) => {
|
||||||
|
let relativePath = path.relative(fullPath, pathname);
|
||||||
|
const { dir, name } = path.parse(relativePath);
|
||||||
|
return path.join(dir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RunnerResults({collection}) {
|
||||||
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!collection.runnerResult) {
|
||||||
|
setSelectedItem(null);
|
||||||
|
}
|
||||||
|
}, [collection, setSelectedItem]);
|
||||||
|
|
||||||
|
const collectionCopy = cloneDeep(collection);
|
||||||
|
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||||
|
each(items, (item) => {
|
||||||
|
const info = findItemInCollection(collectionCopy, item.uid);
|
||||||
|
|
||||||
|
item.name = info.name;
|
||||||
|
item.type = info.type;
|
||||||
|
item.filename = info.filename;
|
||||||
|
item.pathname = info.pathname;
|
||||||
|
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||||
|
|
||||||
|
if(item.testResults) {
|
||||||
|
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||||
|
|
||||||
|
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||||
|
} else {
|
||||||
|
item.testStatus = 'pass';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className='px-4'>
|
||||||
|
<div className='font-medium mt-6 mb-4 title flex items-center'>
|
||||||
|
Runner
|
||||||
|
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<div className='flex flex-col flex-1'>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.uid}>
|
||||||
|
<div className="item-path mt-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>
|
||||||
|
{item.testStatus === 'pass' ? (
|
||||||
|
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||||
|
) : (
|
||||||
|
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className='mr-1 ml-2'>{item.relativePath}</span>
|
||||||
|
{item.status !== "completed" ? (
|
||||||
|
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5}/>
|
||||||
|
) : (
|
||||||
|
<span className='text-xs link cursor-pointer' onClick={() => setSelectedItem(item)}>
|
||||||
|
(<span className='mr-1'>
|
||||||
|
{get(item.responseReceived, 'status')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{get(item.responseReceived, 'statusText')}
|
||||||
|
</span>)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="pl-8">
|
||||||
|
{item.testResults ? item.testResults.map((result) => (
|
||||||
|
<li key={result.uid} className="py-1">
|
||||||
|
{result.status === 'pass' ? (
|
||||||
|
<span className="test-success flex items-center">
|
||||||
|
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
|
||||||
|
{result.description}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="test-failure flex items-center">
|
||||||
|
<IconX size={18} strokeWidth={2} className="mr-2"/>
|
||||||
|
{result.description}
|
||||||
|
</span>
|
||||||
|
<span className="error-message pl-8 text-xs">
|
||||||
|
{result.error}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)): null}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-1' style={{width: '50%'}}>
|
||||||
|
{selectedItem ? (
|
||||||
|
<div className='flex flex-col w-full overflow-auto'>
|
||||||
|
<div className="flex items-center px-3 mb-4 font-medium">
|
||||||
|
<span className='mr-2'>{selectedItem.relativePath}</span>
|
||||||
|
<span>
|
||||||
|
{selectedItem.testStatus === 'pass' ? (
|
||||||
|
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||||
|
) : (
|
||||||
|
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
|
||||||
|
<ResponsePane item={selectedItem} collection={collection}/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
.bruno-modal-content {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import Modal from 'components/Modal';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import { flattenItems } from 'utils/collections';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onSubmit = (recursive) => {
|
||||||
|
dispatch(runCollectionFolder(collection.uid, item.uid, recursive));
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const runLength = get(item, 'items.length', 0);
|
||||||
|
const items = flattenItems(item.items);
|
||||||
|
const requestItems = items.filter((item) => item.type !== 'folder');
|
||||||
|
const recursiveRunLength = requestItems.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<Modal size="md" title='Collection Runner' hideFooter={true} handleCancel={onClose}>
|
||||||
|
<div className='mb-1'>
|
||||||
|
<span className='font-medium'>Run</span>
|
||||||
|
<span className='ml-1 text-xs'>({runLength.length} 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.length} requests)</span>
|
||||||
|
</div>
|
||||||
|
<div className='mb-8'>
|
||||||
|
This will run all the requests in this folder and all its subfolders.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end bruno-modal-footer">
|
||||||
|
<span className='mr-3'>
|
||||||
|
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||||
|
Recursive Run
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RunCollectionItem;
|
@ -15,6 +15,7 @@ import RequestMethod from './RequestMethod';
|
|||||||
import RenameCollectionItem from './RenameCollectionItem';
|
import RenameCollectionItem from './RenameCollectionItem';
|
||||||
import CloneCollectionItem from './CloneCollectionItem';
|
import CloneCollectionItem from './CloneCollectionItem';
|
||||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||||
|
import RunCollectionItem from './RunCollectionItem';
|
||||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||||
@ -33,6 +34,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||||
|
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag({
|
const [{ isDragging }, drag] = useDrag({
|
||||||
@ -151,6 +153,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
||||||
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||||
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
||||||
|
{runCollectionModalOpen && <RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />}
|
||||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||||
<div className="flex items-center h-full w-full">
|
<div className="flex items-center h-full w-full">
|
||||||
{indents && indents.length
|
{indents && indents.length
|
||||||
@ -211,6 +214,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
>
|
>
|
||||||
New Folder
|
New Folder
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={(e) => {
|
||||||
|
dropdownTippyRef.current.hide();
|
||||||
|
setRunCollectionModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
requestQueuedEvent,
|
requestQueuedEvent,
|
||||||
testResultsEvent,
|
testResultsEvent,
|
||||||
scriptEnvironmentUpdateEvent,
|
scriptEnvironmentUpdateEvent,
|
||||||
collectionRenamedEvent
|
collectionRenamedEvent,
|
||||||
|
runFolderEvent
|
||||||
} from 'providers/ReduxStore/slices/collections';
|
} from 'providers/ReduxStore/slices/collections';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
@ -105,6 +106,10 @@ const useCollectionTreeSync = () => {
|
|||||||
dispatch(collectionRenamedEvent(val));
|
dispatch(collectionRenamedEvent(val));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _runFolderEvent = (val) => {
|
||||||
|
dispatch(runFolderEvent(val));
|
||||||
|
};
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:ready');
|
ipcRenderer.invoke('renderer:ready');
|
||||||
|
|
||||||
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||||
@ -116,6 +121,7 @@ const useCollectionTreeSync = () => {
|
|||||||
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
|
||||||
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
|
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
|
||||||
const removeListener9 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
const removeListener9 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
|
||||||
|
const removeListener10 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeListener1();
|
removeListener1();
|
||||||
@ -127,6 +133,7 @@ const useCollectionTreeSync = () => {
|
|||||||
removeListener7();
|
removeListener7();
|
||||||
removeListener8();
|
removeListener8();
|
||||||
removeListener9();
|
removeListener9();
|
||||||
|
removeListener10();
|
||||||
};
|
};
|
||||||
}, [isElectron]);
|
}, [isElectron]);
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@ import { saveCollectionToIdb } from 'utils/idb';
|
|||||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
resetRunResults,
|
||||||
requestCancelled,
|
requestCancelled,
|
||||||
responseReceived,
|
responseReceived,
|
||||||
newItem as _newItem,
|
newItem as _newItem,
|
||||||
@ -138,6 +139,35 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
|||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!collection) {
|
||||||
|
return reject(new Error('Collection not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionCopy = cloneDeep(collection);
|
||||||
|
const folder = findItemInCollection(collectionCopy, folderUid);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
return reject(new Error('Folder not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||||
|
|
||||||
|
dispatch(resetRunResults({
|
||||||
|
collectionUid: collection.uid
|
||||||
|
}));
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:run-collection-folder', folder, collectionCopy, environment, recursive)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
|
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -682,7 +712,8 @@ export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState)
|
|||||||
uid: uid,
|
uid: uid,
|
||||||
name: name,
|
name: name,
|
||||||
pathname: pathname,
|
pathname: pathname,
|
||||||
items: []
|
items: [],
|
||||||
|
showRunner: false
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -849,6 +849,59 @@ export const collectionsSlice = createSlice({
|
|||||||
if (collection) {
|
if (collection) {
|
||||||
collection.name = newName;
|
collection.name = newName;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
toggleRunnerView: (state, action) => {
|
||||||
|
const { collectionUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
console.log('here');
|
||||||
|
if (collection) {
|
||||||
|
console.log('here2');
|
||||||
|
collection.showRunner = !collection.showRunner;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetRunResults: (state, action) => {
|
||||||
|
const { collectionUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
collection.runnerResult = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runFolderEvent: (state, action) => {
|
||||||
|
const { collectionUid, folderUid, itemUid, type } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const folder = findItemInCollection(collection, folderUid);
|
||||||
|
const request = findItemInCollection(collection, itemUid);
|
||||||
|
|
||||||
|
collection.runnerResult = collection.runnerResult || {items: []};
|
||||||
|
|
||||||
|
if(type === 'request-queued') {
|
||||||
|
collection.runnerResult.items.push({
|
||||||
|
uid: request.uid,
|
||||||
|
status: 'queued'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type === 'request-sent') {
|
||||||
|
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||||
|
item.status = 'running';
|
||||||
|
item.requestSent = action.payload.requestSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type === 'response-received') {
|
||||||
|
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||||
|
item.status = 'completed';
|
||||||
|
item.responseReceived = action.payload.responseReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type === 'test-results') {
|
||||||
|
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||||
|
item.testResults = action.payload.testResults;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -901,7 +954,10 @@ export const {
|
|||||||
collectionUnlinkDirectoryEvent,
|
collectionUnlinkDirectoryEvent,
|
||||||
collectionAddEnvFileEvent,
|
collectionAddEnvFileEvent,
|
||||||
testResultsEvent,
|
testResultsEvent,
|
||||||
collectionRenamedEvent
|
collectionRenamedEvent,
|
||||||
|
toggleRunnerView,
|
||||||
|
resetRunResults,
|
||||||
|
runFolderEvent
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
@ -36,11 +36,14 @@ export const safeParseJSON = (str) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const safeStringifyJSON = (obj) => {
|
export const safeStringifyJSON = (obj, indent=false) => {
|
||||||
if(!obj) {
|
if(!obj) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if(indent) {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
}
|
||||||
return JSON.stringify(obj);
|
return JSON.stringify(obj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return obj;
|
return obj;
|
||||||
|
80
packages/bruno-electron/src/ipc/network/helper.js
Normal file
80
packages/bruno-electron/src/ipc/network/helper.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const {
|
||||||
|
each,
|
||||||
|
filter
|
||||||
|
} = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
|
const sortCollection = (collection) => {
|
||||||
|
const items = collection.items || [];
|
||||||
|
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||||
|
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||||
|
|
||||||
|
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||||
|
|
||||||
|
collection.items = folderItems.concat(requestItems);
|
||||||
|
|
||||||
|
each(folderItems, (item) => {
|
||||||
|
sortCollection(item);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortFolder = (folder = {}) => {
|
||||||
|
const items = folder.items || [];
|
||||||
|
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||||
|
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||||
|
|
||||||
|
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||||
|
|
||||||
|
folder.items = folderItems.concat(requestItems);
|
||||||
|
|
||||||
|
each(folderItems, (item) => {
|
||||||
|
sortFolder(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findItemInCollection = (collection, itemId) => {
|
||||||
|
let item = null;
|
||||||
|
|
||||||
|
if (collection.uid === itemId) {
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection.items && collection.items.length) {
|
||||||
|
collection.items.forEach((item) => {
|
||||||
|
if (item.uid === itemId) {
|
||||||
|
item = item;
|
||||||
|
} else if (item.type === 'folder') {
|
||||||
|
item = findItemInCollection(item, itemId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllRequestsInFolderRecursively = (folder = {}) => {
|
||||||
|
let requests = [];
|
||||||
|
|
||||||
|
if (folder.items && folder.items.length) {
|
||||||
|
folder.items.forEach((item) => {
|
||||||
|
if (item.type !== 'folder') {
|
||||||
|
requests.push(item);
|
||||||
|
} else {
|
||||||
|
requests = requests.concat(getAllRequestsInFolderRecursively(item));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sortCollection,
|
||||||
|
sortFolder,
|
||||||
|
findItemInCollection,
|
||||||
|
getAllRequestsInFolderRecursively
|
||||||
|
};
|
@ -9,6 +9,10 @@ const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-requ
|
|||||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||||
const { uuid } = require('../../utils/common');
|
const { uuid } = require('../../utils/common');
|
||||||
const interpolateVars = require('./interpolate-vars');
|
const interpolateVars = require('./interpolate-vars');
|
||||||
|
const {
|
||||||
|
sortFolder,
|
||||||
|
getAllRequestsInFolderRecursively
|
||||||
|
} = require('./helper');
|
||||||
|
|
||||||
// override the default escape function to prevent escaping
|
// override the default escape function to prevent escaping
|
||||||
Mustache.escape = function (value) {
|
Mustache.escape = function (value) {
|
||||||
@ -47,6 +51,22 @@ const getEnvVars = (environment = {}) => {
|
|||||||
return envVars;
|
return envVars;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSize = (data) => {
|
||||||
|
if(!data) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof data === 'string') {
|
||||||
|
return Buffer.byteLength(data, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof data === 'object') {
|
||||||
|
return Buffer.byteLength(JSON.stringify(data), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||||
// handler for sending http request
|
// handler for sending http request
|
||||||
ipcMain.handle('send-http-request', async (event, item, collectionUid, collectionPath, environment) => {
|
ipcMain.handle('send-http-request', async (event, item, collectionUid, collectionPath, environment) => {
|
||||||
@ -197,6 +217,141 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:run-collection-folder', async (event, folder, collection, environment, recursive) => {
|
||||||
|
const collectionUid = collection.uid;
|
||||||
|
const collectionPath = collection.pathname;
|
||||||
|
const folderUid = folder.uid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const envVars = getEnvVars(environment);
|
||||||
|
let folderRequests = [];
|
||||||
|
|
||||||
|
if(recursive) {
|
||||||
|
let sortedFolder = sortFolder(folder);
|
||||||
|
folderRequests = getAllRequestsInFolderRecursively(sortedFolder);
|
||||||
|
console.log('-----sortedFolder------');
|
||||||
|
console.log(sortedFolder);
|
||||||
|
console.log('-----folderRequests------');
|
||||||
|
console.log(folderRequests);
|
||||||
|
} else {
|
||||||
|
each(folder.items, (item) => {
|
||||||
|
if(item.request) {
|
||||||
|
folderRequests.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort requests by seq property
|
||||||
|
folderRequests.sort((a, b) => {
|
||||||
|
return a.seq - b.seq;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let item of folderRequests) {
|
||||||
|
const itemUid = item.uid;
|
||||||
|
const eventData = {
|
||||||
|
collectionUid,
|
||||||
|
folderUid,
|
||||||
|
itemUid
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'request-queued',
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
|
||||||
|
const _request = item.draft ? item.draft.request : item.request;
|
||||||
|
const request = prepareRequest(_request);
|
||||||
|
|
||||||
|
// make axios work in node using form data
|
||||||
|
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||||
|
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||||
|
const form = new FormData();
|
||||||
|
forOwn(request.data, (value, key) => {
|
||||||
|
form.append(key, value);
|
||||||
|
});
|
||||||
|
extend(request.headers, form.getHeaders());
|
||||||
|
request.data = form;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolateVars(request, envVars);
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
// i have no clue why electron can't send the request object
|
||||||
|
// without safeParseJSON(safeStringifyJSON(request.data))
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'request-sent',
|
||||||
|
requestSent: {
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers,
|
||||||
|
data: safeParseJSON(safeStringifyJSON(request.data))
|
||||||
|
},
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeStart = Date.now();
|
||||||
|
const response = await axios(request);
|
||||||
|
const timeEnd = Date.now();
|
||||||
|
|
||||||
|
if(request.script && request.script.length) {
|
||||||
|
let script = request.script + '\n if (typeof onResponse === "function") {onResponse(__brunoResponse);}';
|
||||||
|
const scriptRuntime = new ScriptRuntime();
|
||||||
|
const result = scriptRuntime.runResponseScript(script, response, envVars, collectionPath);
|
||||||
|
|
||||||
|
mainWindow.webContents.send('main:script-environment-update', {
|
||||||
|
environment: result.environment,
|
||||||
|
collectionUid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFile = get(item, 'request.tests');
|
||||||
|
if(testFile && testFile.length) {
|
||||||
|
const testRuntime = new TestRuntime();
|
||||||
|
const result = testRuntime.runTests(testFile, request, response, envVars, collectionPath);
|
||||||
|
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'test-results',
|
||||||
|
testResults: result.results,
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'response-received',
|
||||||
|
...eventData,
|
||||||
|
responseReceived: {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: Object.entries(response.headers),
|
||||||
|
duration: timeEnd - timeStart,
|
||||||
|
size: response.headers['content-length'] || getSize(response.data),
|
||||||
|
data: response.data,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'error',
|
||||||
|
error,
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
|
type: 'error',
|
||||||
|
data: {
|
||||||
|
error : {
|
||||||
|
message: error.message
|
||||||
|
},
|
||||||
|
collectionUid,
|
||||||
|
folderUid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = registerNetworkIpc;
|
module.exports = registerNetworkIpc;
|
||||||
|
@ -85,7 +85,11 @@ const collectionSchema = Yup.object({
|
|||||||
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')
|
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')
|
||||||
.nullable(),
|
.nullable(),
|
||||||
environments: environmentsSchema,
|
environments: environmentsSchema,
|
||||||
pathname: Yup.string().nullable()
|
pathname: Yup.string().nullable(),
|
||||||
|
showRunner: Yup.boolean(),
|
||||||
|
runnerResult: Yup.object({
|
||||||
|
items: Yup.array()
|
||||||
|
})
|
||||||
}).noUnknown(true).strict();
|
}).noUnknown(true).strict();
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user