mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
* #1884 - Add support for text/event-stream content-type * #1884 - Fix bugs with streaming Fix bug when streaming response is not ok Fix bug when clearing response of streaming request Show text signaling that the response is being streamed in the reponse status Update response size when new data is streamed in * #1884 - Fix multiple requests when spamming send button * #1884 - Add time counter for streamed response and fix final time * #1884 - Run post script only at end of streamed request * #1884 - add support for automatic "upgrade" to streaming data * #1884 - adjustments for stopwatch in stream implementation and remove unused imports * #1884 - fix imports indentation in useIpcEvents.js * #1884 - remove stream data ended export function from collections --------- Co-authored-by: Siddharth Gelera <ahoy@barelyhuman.dev>
This commit is contained in:
@@ -5,7 +5,7 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode, IconX } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import { hasRequestChanges } from 'utils/collections';
|
||||
@@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center justify-center h-full w-16">
|
||||
<span className="text-xs text-indigo-500 font-bold">gRPC</span>
|
||||
</div>
|
||||
|
||||
|
||||
) : (
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
)}
|
||||
@@ -149,7 +149,13 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
|
||||
{
|
||||
item.response?.hasStreamRunning ? (
|
||||
<IconX color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
) : (
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
|
||||
@@ -10,7 +10,7 @@ import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';
|
||||
import Welcome from 'components/Welcome';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl/index';
|
||||
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
|
||||
@@ -74,8 +74,7 @@ const RequestTabPanel = () => {
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
|
||||
); // 2.2 is intentional to make both panes appear to be of equal width
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 is intentional to make both panes appear to be of equal width
|
||||
const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
@@ -141,12 +140,10 @@ const RequestTabPanel = () => {
|
||||
setDragging(false);
|
||||
if (!isVerticalLayout) {
|
||||
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - mainRect.left
|
||||
})
|
||||
);
|
||||
dispatch(updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - mainRect.left
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -263,11 +260,17 @@ const RequestTabPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
);
|
||||
if (item.response?.hasStreamRunning) {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
} else if (item.requestState !== 'sending' && item.requestState !== 'queued') {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: reaper, improve selection of panes
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseStopWatch = ({ startMillis }) => {
|
||||
const [milliseconds, setMilliseconds] = useState(startMillis);
|
||||
|
||||
const tickInterval = 100;
|
||||
const tick = () => {
|
||||
setMilliseconds(_milliseconds => _milliseconds + tickInterval);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timerID = setInterval(() => {
|
||||
tick()
|
||||
}, tickInterval);
|
||||
return () => {
|
||||
clearTimeout(timerID);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
let secondsFormatted = `${seconds.toFixed(1)}s`;
|
||||
let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast
|
||||
return <StyledWrapper className="ml-4" style={{width: `${width}rem`}}>{secondsFormatted}</StyledWrapper>;
|
||||
};
|
||||
|
||||
export default React.memo(ResponseStopWatch);
|
||||
@@ -4,7 +4,7 @@ import statusCodePhraseMap from './get-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Todo: text-error class is not getting pulled in for 500 errors
|
||||
const StatusCode = ({ status, statusText }) => {
|
||||
const StatusCode = ({ status, statusText, isStreaming }) => {
|
||||
const getTabClassname = (status) => {
|
||||
return classnames('ml-2', {
|
||||
'text-ok': status >= 100 && status < 200,
|
||||
@@ -17,7 +17,7 @@ const StatusCode = ({ status, statusText }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`} data-testid="response-status-code">
|
||||
{status} {statusText || statusCodePhraseMap[status]}
|
||||
{status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,8 @@ import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import ResponseBookmark from 'src/components/ResponsePane/ResponseBookmark';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import StopWatch from 'components/StopWatch';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
@@ -87,15 +89,17 @@ const ResponsePane = ({ item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>;
|
||||
return (
|
||||
<TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -184,7 +188,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<StatusCode status={response.status} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.hasStreamRunning} />
|
||||
{item.response?.hasStreamRunning ? (
|
||||
<ResponseStopWatch startMillis={response.duration} />
|
||||
) : <ResponseTime duration={response.duration} />}
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
@@ -193,7 +200,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
|
||||
className="flex flex-col min-h-0 relative px-4 auto overflow-auto"
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
@@ -206,9 +213,9 @@ const ResponsePane = ({ item, collection }) => {
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
runFolderEvent,
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
scriptEnvironmentUpdateEvent,
|
||||
streamDataReceived
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -137,8 +139,8 @@ const useIpcEvents = () => {
|
||||
dispatch(processEnvUpdateEvent(val));
|
||||
});
|
||||
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
dispatch(addLog({
|
||||
type: val.type,
|
||||
args: val.args,
|
||||
@@ -188,6 +190,14 @@ const useIpcEvents = () => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => {
|
||||
dispatch(streamDataReceived(val));
|
||||
});
|
||||
|
||||
const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => {
|
||||
dispatch(requestCancelled(val));
|
||||
});
|
||||
|
||||
const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
|
||||
dispatch(updateCollectionLoadingState(val));
|
||||
});
|
||||
@@ -212,6 +222,8 @@ const useIpcEvents = () => {
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
removeHttpStreamNewDataListener();
|
||||
removeHttpStreamEndListener();
|
||||
removeCollectionLoadingStateListener();
|
||||
removePersistentEnvVariablesUpdateListener();
|
||||
removeSystemResourcesListener();
|
||||
|
||||
@@ -83,8 +83,8 @@ const initiatedGrpcResponse = {
|
||||
isError: false,
|
||||
duration: 0,
|
||||
responses: [],
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const initiatedWsResponse = {
|
||||
status: 'PENDING',
|
||||
@@ -380,7 +380,15 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.response = null;
|
||||
if (item.response?.hasStreamRunning) {
|
||||
item.response.hasStreamRunning = null;
|
||||
|
||||
const startTimestamp = item.requestSent.timestamp;
|
||||
item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration;
|
||||
} else {
|
||||
item.response = null;
|
||||
}
|
||||
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
@@ -389,22 +397,22 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
item.cancelTokenUid = item.response.hasStreamRunning ? item.cancelTokenUid : null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure timestamp is a number (milliseconds since epoch)
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
|
||||
// Append the new timeline entry with numeric timestamp
|
||||
@@ -427,7 +435,7 @@ export const collectionsSlice = createSlice({
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
@@ -447,7 +455,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
type: 'request',
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -456,36 +464,34 @@ export const collectionsSlice = createSlice({
|
||||
data: {
|
||||
request: eventData || item.requestSent || item.request,
|
||||
timestamp: Date.now(),
|
||||
eventData: eventData,
|
||||
eventData: eventData
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
grpcResponseReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
|
||||
// Get current response state or create initial state
|
||||
const currentResponse = item.response || initiatedGrpcResponse
|
||||
const currentResponse = item.response || initiatedGrpcResponse;
|
||||
const timestamp = item?.requestSent?.timestamp;
|
||||
let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };
|
||||
|
||||
|
||||
// Process based on event type
|
||||
switch (eventType) {
|
||||
case 'response':
|
||||
const { error, res } = eventData;
|
||||
|
||||
|
||||
// Handle error if present
|
||||
if (error) {
|
||||
const errorCode = error.code || 2; // Default to UNKNOWN if no code
|
||||
|
||||
|
||||
updatedResponse.error = error.details || 'gRPC error occurred';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
@@ -494,72 +500,72 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
// Add response to list
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
: [...(currentResponse?.responses || [])];
|
||||
break;
|
||||
|
||||
|
||||
case 'metadata':
|
||||
updatedResponse.headers = eventData.metadata;
|
||||
updatedResponse.metadata = eventData.metadata;
|
||||
break;
|
||||
|
||||
|
||||
case 'status':
|
||||
// Extract status info
|
||||
const statusCode = eventData.status?.code;
|
||||
const statusDetails = eventData.status?.details;
|
||||
const statusMetadata = eventData.status?.metadata;
|
||||
|
||||
|
||||
// Set status based on actual code and details
|
||||
updatedResponse.statusCode = statusCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = statusDetails;
|
||||
updatedResponse.statusDetails = eventData.status;
|
||||
|
||||
|
||||
// Store trailers (status metadata)
|
||||
if (statusMetadata) {
|
||||
updatedResponse.trailers = statusMetadata;
|
||||
}
|
||||
|
||||
|
||||
// Handle error status (non-zero code)
|
||||
if (statusCode !== 0) {
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
// Extract error details
|
||||
const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code
|
||||
const errorDetails = eventData.error?.details || eventData.error?.message;
|
||||
const errorMetadata = eventData.error?.metadata;
|
||||
|
||||
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = errorDetails || 'Unknown gRPC error';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = errorDetails;
|
||||
|
||||
|
||||
// Store error metadata as trailers if present
|
||||
if (errorMetadata) {
|
||||
updatedResponse.trailers = errorMetadata;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'end':
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
break;
|
||||
|
||||
|
||||
case 'cancel':
|
||||
updatedResponse.statusCode = 1; // CANCELLED
|
||||
updatedResponse.statusText = 'CANCELLED';
|
||||
updatedResponse.statusDescription = 'Stream cancelled by client or server';
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
item.requestState = 'received';
|
||||
item.response = updatedResponse;
|
||||
|
||||
@@ -570,7 +576,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Append the new timeline entry with specific gRPC event type
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
type: 'request',
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -580,7 +586,7 @@ export const collectionsSlice = createSlice({
|
||||
request: item.requestSent || item.request,
|
||||
response: updatedResponse,
|
||||
eventData: eventData, // Store the original event data
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -590,6 +596,12 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
if (item.response && item.response.hasStreamRunning) {
|
||||
item.response.data = '';
|
||||
item.response.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
item.response = null;
|
||||
}
|
||||
}
|
||||
@@ -916,7 +928,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || [];
|
||||
const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || [];
|
||||
const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name,
|
||||
@@ -930,9 +942,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Update the request URL to reflect the new query params
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(
|
||||
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
|
||||
);
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||
|
||||
// If there are enabled query params, append them to the URL
|
||||
if (query && query.length) {
|
||||
@@ -1163,7 +1173,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({
|
||||
item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1205,8 +1215,8 @@ export const collectionsSlice = createSlice({
|
||||
if (!folder || !isItemAFolder(folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
|
||||
folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1487,7 +1497,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
|
||||
switch (item.draft.request.body.mode) {
|
||||
case 'json': {
|
||||
item.draft.request.body.json = action.payload.content;
|
||||
@@ -1624,7 +1634,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
@@ -1875,7 +1885,7 @@ export const collectionsSlice = createSlice({
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(collection, 'draft.root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
break;
|
||||
case 'oauth2':
|
||||
set(collection, 'draft.root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
@@ -2604,7 +2614,7 @@ export const collectionsSlice = createSlice({
|
||||
const { requestUid, itemUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
|
||||
@@ -2637,7 +2647,7 @@ export const collectionsSlice = createSlice({
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'test-script-execution') {
|
||||
if (type === 'test-script-execution') {
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
@@ -2652,7 +2662,7 @@ export const collectionsSlice = createSlice({
|
||||
if (type === 'request-sent') {
|
||||
const { cancelTokenUid, requestSent } = action.payload;
|
||||
item.requestSent = requestSent;
|
||||
|
||||
|
||||
// sometimes the response is received before the request-sent event arrives
|
||||
if (item.requestState === 'queued') {
|
||||
item.requestState = 'sending';
|
||||
@@ -2669,12 +2679,12 @@ export const collectionsSlice = createSlice({
|
||||
const { results } = action.payload;
|
||||
item.testResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-pre-request') {
|
||||
const { results } = action.payload;
|
||||
item.preRequestTestResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-post-response') {
|
||||
const { results } = action.payload;
|
||||
item.postResponseTestResults = results;
|
||||
@@ -2788,7 +2798,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.runnerTags = { include: [], exclude: [] }
|
||||
collection.runnerTags = { include: [], exclude: [] };
|
||||
collection.runnerTagsEnabled = false;
|
||||
collection.runnerConfiguration = null;
|
||||
}
|
||||
@@ -2927,7 +2937,7 @@ export const collectionsSlice = createSlice({
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
@@ -2936,7 +2946,16 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'draft.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
streamDataReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, data } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
item.response.data = data.data + (item.response.data || '');
|
||||
item.response.size = data.data?.length + (item.response.size || 0);
|
||||
}
|
||||
},
|
||||
addRequestTag: (state, action) => {
|
||||
const { tag, collectionUid, itemUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -2978,7 +2997,7 @@ export const collectionsSlice = createSlice({
|
||||
updateCollectionTagsList: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||
}
|
||||
@@ -3298,6 +3317,7 @@ export const {
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
streamDataReceived,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
|
||||
@@ -20,7 +20,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
timeline: response.timeline,
|
||||
hasStreamRunning: response.hasStreamRunning
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
@@ -31,19 +32,17 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
startGrpcRequest(item, collection, environment, runtimeVariables)
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -83,19 +82,19 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const request = item.draft ? item.draft : item;
|
||||
|
||||
|
||||
ipcRenderer.invoke('grpc:start-connection', {
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
runtimeVariables
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -188,7 +187,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
resolve(response.isActive);
|
||||
} else {
|
||||
@@ -197,7 +196,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error('Failed to check connection status:', err);
|
||||
// On error, assume the connection is not active
|
||||
resolve(false);
|
||||
@@ -215,14 +214,14 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
|
||||
ipcRenderer.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user