mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 23:43:15 +01:00
feat: cancel running request (resolves #26)
This commit is contained in:
parent
097a6240ad
commit
410bc70318
@ -8,7 +8,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
||||
@ -16,7 +16,7 @@ const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const sendNetworkRequest = async () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleRun = async () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(updateRequestPaneTab({
|
||||
@ -67,7 +67,7 @@ const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
||||
<QueryUrl
|
||||
item = {item}
|
||||
collection={collection}
|
||||
handleRun={sendNetworkRequest}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
|
@ -139,9 +139,9 @@ const RequestTabPanel = () => {
|
||||
<section className="response-pane flex-grow mt-2">
|
||||
<ResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
rightPaneWidth={rightPaneWidth}
|
||||
response={item.response}
|
||||
isLoading={item.response && item.response.state === 'sending' ? true : false}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
|
@ -1,9 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IconRefresh } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StopWatch from '../../StopWatch';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = () => {
|
||||
const ResponseLoadingOverlay = ({item, collection}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCancelRequest = () => {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 px-3 w-full">
|
||||
<div className="overlay">
|
||||
@ -14,6 +22,7 @@ const QueryResult = () => {
|
||||
</div>
|
||||
<IconRefresh size={24} className="animate-spin"/>
|
||||
<button
|
||||
onClick={handleCancelRequest}
|
||||
className="mt-4 uppercase bg-gray-200 active:bg-blueGray-600 text-xs px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="button"
|
||||
>
|
||||
Cancel Request
|
||||
@ -23,4 +32,4 @@ const QueryResult = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryResult;
|
||||
export default ResponseLoadingOverlay;
|
||||
|
@ -12,10 +12,11 @@ import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
|
||||
const ResponsePane = ({rightPaneWidth, item, collection}) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = item.response && item.response.state === 'sending';
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(updateResponsePaneTab({
|
||||
@ -51,10 +52,11 @@ const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
|
||||
if(isLoading) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Overlay />
|
||||
<Overlay item={item} collection={collection}/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if(response.state !== 'success') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
|
@ -1,3 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
@ -6,9 +7,14 @@ import {
|
||||
transformCollectionToSaveToIdb
|
||||
} from 'utils/collections';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
|
||||
import { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
||||
import { sendNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
requestSent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
createCollection as _createCollection,
|
||||
renameCollection as _renameCollection,
|
||||
deleteCollection as _deleteCollection,
|
||||
@ -108,3 +114,38 @@ export const deleteCollection = (collectionUid) => (dispatch, getState) => {
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const sendRequest = (item, collectionUid) => (dispatch) => {
|
||||
const axiosRequest = axios.CancelToken.source();
|
||||
const cancelTokenUid = uuid();
|
||||
|
||||
saveCancelToken(cancelTokenUid, axiosRequest);
|
||||
dispatch(requestSent({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
cancelTokenUid: cancelTokenUid
|
||||
}));
|
||||
|
||||
sendNetworkRequest(item, {cancelToken: axiosRequest.token})
|
||||
.then((response) => {
|
||||
if(response && response.status !== -1) {
|
||||
return dispatch(responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
response: response
|
||||
}));
|
||||
}
|
||||
})
|
||||
.then(() => deleteCancelToken(cancelTokenUid))
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) => {
|
||||
if(cancelTokenUid && cancelTokens[cancelTokenUid]) {
|
||||
cancelTokens[cancelTokenUid].cancel();
|
||||
dispatch(requestCancelled({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}))
|
||||
}
|
||||
};
|
@ -6,7 +6,6 @@ import each from 'lodash/each';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import splitOnFirst from 'split-on-first';
|
||||
import { sendNetworkRequest } from 'utils/network';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
findItemInCollection,
|
||||
@ -99,24 +98,39 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
_requestSent: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
requestSent: (state, action) => {
|
||||
const {itemUid, collectionUid, cancelTokenUid} = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if(collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if(item) {
|
||||
item.response = item.response || {};
|
||||
item.response.state = 'sending';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
}
|
||||
},
|
||||
_responseReceived: (state, action) => {
|
||||
requestCancelled: (state, action) => {
|
||||
const {itemUid, collectionUid} = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if(collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if(item) {
|
||||
item.response = null;
|
||||
item.cancelTokenUid = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if(collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if(item) {
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -535,8 +549,9 @@ export const {
|
||||
_deleteItem,
|
||||
_renameItem,
|
||||
_cloneItem,
|
||||
_requestSent,
|
||||
_responseReceived,
|
||||
requestSent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
_saveRequest,
|
||||
newEphermalHttpRequest,
|
||||
collectionClicked,
|
||||
@ -567,20 +582,6 @@ export const loadCollectionsFromIdb = () => (dispatch) => {
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const sendRequest = (item, collectionUid) => (dispatch) => {
|
||||
dispatch(_requestSent({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid
|
||||
}));
|
||||
sendNetworkRequest(item)
|
||||
.then((response) => dispatch(_responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
response: response
|
||||
})))
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
14
packages/bruno-app/src/utils/network/cancelTokens.js
Normal file
14
packages/bruno-app/src/utils/network/cancelTokens.js
Normal file
@ -0,0 +1,14 @@
|
||||
// we maintain cancel tokens for a request separately as redux does not recommend to store
|
||||
// non-serializable value in the store
|
||||
|
||||
const cancelTokens = {};
|
||||
|
||||
export default cancelTokens;
|
||||
|
||||
export const saveCancelToken = (uid, axiosRequest) => {
|
||||
cancelTokens[uid] = axiosRequest;
|
||||
}
|
||||
|
||||
export const deleteCancelToken = (uid) => {
|
||||
delete cancelTokens[uid];
|
||||
}
|
@ -4,11 +4,11 @@ import qs from 'qs';
|
||||
import { rawRequest, gql } from 'graphql';
|
||||
import { sendHttpRequestInBrowser } from './browser';
|
||||
|
||||
const sendNetworkRequest = async (item) => {
|
||||
const sendNetworkRequest = async (item, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(item.type === 'http') {
|
||||
const timeStart = Date.now();
|
||||
sendHttpRequest(item.draft ? item.draft.request : item.request)
|
||||
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
||||
.then((response) => {
|
||||
const timeEnd = Date.now();
|
||||
resolve({
|
||||
@ -25,7 +25,7 @@ const sendNetworkRequest = async (item) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (request) => {
|
||||
const sendHttpRequest = async (request, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
@ -36,57 +36,61 @@ const sendHttpRequest = async (request) => {
|
||||
}
|
||||
});
|
||||
|
||||
let options = {
|
||||
let axiosRequest = {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
if(options && options.cancelToken) {
|
||||
axiosRequest.cancelToken = options.cancelToken;
|
||||
}
|
||||
|
||||
if(request.body.mode === 'json') {
|
||||
options.headers['content-type'] = 'application/json';
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
try {
|
||||
options.data = JSON.parse(request.body.json);
|
||||
axiosRequest.data = JSON.parse(request.body.json);
|
||||
} catch (ex) {
|
||||
options.data = request.body.json;
|
||||
axiosRequest.data = request.body.json;
|
||||
}
|
||||
}
|
||||
|
||||
if(request.body.mode === 'text') {
|
||||
options.headers['content-type'] = 'text/plain';
|
||||
options.data = request.body.text;
|
||||
axiosRequest.headers['content-type'] = 'text/plain';
|
||||
axiosRequest.data = request.body.text;
|
||||
}
|
||||
|
||||
if(request.body.mode === 'xml') {
|
||||
options.headers['content-type'] = 'text/xml';
|
||||
options.data = request.body.xml;
|
||||
axiosRequest.headers['content-type'] = 'text/xml';
|
||||
axiosRequest.data = request.body.xml;
|
||||
}
|
||||
|
||||
if(request.body.mode === 'formUrlEncoded') {
|
||||
options.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
const params = {};
|
||||
const enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
|
||||
each(enabledParams, (p) => params[p.name] = p.value);
|
||||
options.data = qs.stringify(params);
|
||||
axiosRequest.data = qs.stringify(params);
|
||||
}
|
||||
|
||||
if(request.body.mode === 'multipartForm') {
|
||||
const params = {};
|
||||
const enabledParams = filter(request.body.multipartForm, p => p.enabled);
|
||||
each(enabledParams, (p) => params[p.name] = p.value);
|
||||
options.headers['content-type'] = 'multipart/form-data';
|
||||
options.data = params;
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
console.log('>>> Sending Request');
|
||||
console.log(options);
|
||||
console.log(axiosRequest);
|
||||
|
||||
// Todo: Choose based on platform (web/desktop)
|
||||
sendHttpRequestInBrowser(options)
|
||||
sendHttpRequestInBrowser(axiosRequest)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
// ipcRenderer
|
||||
// .invoke('send-http', options)
|
||||
// .invoke('send-http', axiosRequest)
|
||||
// .then(resolve)
|
||||
// .catch(reject);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user