forked from extern/bruno
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 RequestBody from 'components/RequestPane/RequestBody';
|
||||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
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';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
||||||
@ -16,7 +16,7 @@ const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
|||||||
const tabs = useSelector((state) => state.tabs.tabs);
|
const tabs = useSelector((state) => state.tabs.tabs);
|
||||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
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) => {
|
const selectTab = (tab) => {
|
||||||
dispatch(updateRequestPaneTab({
|
dispatch(updateRequestPaneTab({
|
||||||
@ -67,7 +67,7 @@ const HttpRequestPane = ({item, collection, leftPaneWidth}) => {
|
|||||||
<QueryUrl
|
<QueryUrl
|
||||||
item = {item}
|
item = {item}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
handleRun={sendNetworkRequest}
|
handleRun={handleRun}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center tabs" role="tablist">
|
<div className="flex items-center tabs" role="tablist">
|
||||||
|
@ -139,9 +139,9 @@ const RequestTabPanel = () => {
|
|||||||
<section className="response-pane flex-grow mt-2">
|
<section className="response-pane flex-grow mt-2">
|
||||||
<ResponsePane
|
<ResponsePane
|
||||||
item={item}
|
item={item}
|
||||||
|
collection={collection}
|
||||||
rightPaneWidth={rightPaneWidth}
|
rightPaneWidth={rightPaneWidth}
|
||||||
response={item.response}
|
response={item.response}
|
||||||
isLoading={item.response && item.response.state === 'sending' ? true : false}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconRefresh } from '@tabler/icons';
|
import { IconRefresh } from '@tabler/icons';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import StopWatch from '../../StopWatch';
|
import StopWatch from '../../StopWatch';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const QueryResult = () => {
|
const ResponseLoadingOverlay = ({item, collection}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleCancelRequest = () => {
|
||||||
|
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="mt-4 px-3 w-full">
|
<StyledWrapper className="mt-4 px-3 w-full">
|
||||||
<div className="overlay">
|
<div className="overlay">
|
||||||
@ -14,6 +22,7 @@ const QueryResult = () => {
|
|||||||
</div>
|
</div>
|
||||||
<IconRefresh size={24} className="animate-spin"/>
|
<IconRefresh size={24} className="animate-spin"/>
|
||||||
<button
|
<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"
|
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
|
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 ResponseSize from './ResponseSize';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
|
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);
|
||||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||||
|
const isLoading = item.response && item.response.state === 'sending';
|
||||||
|
|
||||||
const selectTab = (tab) => {
|
const selectTab = (tab) => {
|
||||||
dispatch(updateResponsePaneTab({
|
dispatch(updateResponsePaneTab({
|
||||||
@ -51,10 +52,11 @@ const ResponsePane = ({rightPaneWidth, item, isLoading}) => {
|
|||||||
if(isLoading) {
|
if(isLoading) {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex h-full relative">
|
<StyledWrapper className="flex h-full relative">
|
||||||
<Overlay />
|
<Overlay item={item} collection={collection}/>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.state !== 'success') {
|
if(response.state !== 'success') {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex h-full relative">
|
<StyledWrapper className="flex h-full relative">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import axios from 'axios';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import {
|
import {
|
||||||
@ -6,9 +7,14 @@ import {
|
|||||||
transformCollectionToSaveToIdb
|
transformCollectionToSaveToIdb
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { waitForNextTick } from 'utils/common';
|
import { waitForNextTick } from 'utils/common';
|
||||||
|
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
|
||||||
import { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
import { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
||||||
|
import { sendNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
requestSent,
|
||||||
|
requestCancelled,
|
||||||
|
responseReceived,
|
||||||
createCollection as _createCollection,
|
createCollection as _createCollection,
|
||||||
renameCollection as _renameCollection,
|
renameCollection as _renameCollection,
|
||||||
deleteCollection as _deleteCollection,
|
deleteCollection as _deleteCollection,
|
||||||
@ -107,4 +113,39 @@ export const deleteCollection = (collectionUid) => (dispatch, getState) => {
|
|||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(reject);
|
.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 cloneDeep from 'lodash/cloneDeep';
|
||||||
import { createSlice } from '@reduxjs/toolkit'
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
import splitOnFirst from 'split-on-first';
|
import splitOnFirst from 'split-on-first';
|
||||||
import { sendNetworkRequest } from 'utils/network';
|
|
||||||
import {
|
import {
|
||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
findItemInCollection,
|
findItemInCollection,
|
||||||
@ -99,24 +98,39 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_requestSent: (state, action) => {
|
requestSent: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const {itemUid, collectionUid, cancelTokenUid} = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
if(collection) {
|
if(collection) {
|
||||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
const item = findItemInCollection(collection, itemUid);
|
||||||
if(item) {
|
if(item) {
|
||||||
item.response = item.response || {};
|
item.response = item.response || {};
|
||||||
item.response.state = 'sending';
|
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);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
if(collection) {
|
if(collection) {
|
||||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||||
if(item) {
|
if(item) {
|
||||||
item.response = action.payload.response;
|
item.response = action.payload.response;
|
||||||
|
item.cancelTokenUid = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -535,8 +549,9 @@ export const {
|
|||||||
_deleteItem,
|
_deleteItem,
|
||||||
_renameItem,
|
_renameItem,
|
||||||
_cloneItem,
|
_cloneItem,
|
||||||
_requestSent,
|
requestSent,
|
||||||
_responseReceived,
|
requestCancelled,
|
||||||
|
responseReceived,
|
||||||
_saveRequest,
|
_saveRequest,
|
||||||
newEphermalHttpRequest,
|
newEphermalHttpRequest,
|
||||||
collectionClicked,
|
collectionClicked,
|
||||||
@ -567,20 +582,6 @@ export const loadCollectionsFromIdb = () => (dispatch) => {
|
|||||||
.catch((err) => console.log(err));
|
.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) => {
|
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
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 { rawRequest, gql } from 'graphql';
|
||||||
import { sendHttpRequestInBrowser } from './browser';
|
import { sendHttpRequestInBrowser } from './browser';
|
||||||
|
|
||||||
const sendNetworkRequest = async (item) => {
|
const sendNetworkRequest = async (item, options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if(item.type === 'http') {
|
if(item.type === 'http') {
|
||||||
const timeStart = Date.now();
|
const timeStart = Date.now();
|
||||||
sendHttpRequest(item.draft ? item.draft.request : item.request)
|
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const timeEnd = Date.now();
|
const timeEnd = Date.now();
|
||||||
resolve({
|
resolve({
|
||||||
@ -25,7 +25,7 @@ const sendNetworkRequest = async (item) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendHttpRequest = async (request) => {
|
const sendHttpRequest = async (request, options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
@ -36,57 +36,61 @@ const sendHttpRequest = async (request) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let options = {
|
let axiosRequest = {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: request.url,
|
url: request.url,
|
||||||
headers: headers
|
headers: headers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(options && options.cancelToken) {
|
||||||
|
axiosRequest.cancelToken = options.cancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'json') {
|
if(request.body.mode === 'json') {
|
||||||
options.headers['content-type'] = 'application/json';
|
axiosRequest.headers['content-type'] = 'application/json';
|
||||||
try {
|
try {
|
||||||
options.data = JSON.parse(request.body.json);
|
axiosRequest.data = JSON.parse(request.body.json);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
options.data = request.body.json;
|
axiosRequest.data = request.body.json;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'text') {
|
if(request.body.mode === 'text') {
|
||||||
options.headers['content-type'] = 'text/plain';
|
axiosRequest.headers['content-type'] = 'text/plain';
|
||||||
options.data = request.body.text;
|
axiosRequest.data = request.body.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'xml') {
|
if(request.body.mode === 'xml') {
|
||||||
options.headers['content-type'] = 'text/xml';
|
axiosRequest.headers['content-type'] = 'text/xml';
|
||||||
options.data = request.body.xml;
|
axiosRequest.data = request.body.xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'formUrlEncoded') {
|
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 params = {};
|
||||||
const enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
|
const enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
|
||||||
each(enabledParams, (p) => params[p.name] = p.value);
|
each(enabledParams, (p) => params[p.name] = p.value);
|
||||||
options.data = qs.stringify(params);
|
axiosRequest.data = qs.stringify(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'multipartForm') {
|
if(request.body.mode === 'multipartForm') {
|
||||||
const params = {};
|
const params = {};
|
||||||
const enabledParams = filter(request.body.multipartForm, p => p.enabled);
|
const enabledParams = filter(request.body.multipartForm, p => p.enabled);
|
||||||
each(enabledParams, (p) => params[p.name] = p.value);
|
each(enabledParams, (p) => params[p.name] = p.value);
|
||||||
options.headers['content-type'] = 'multipart/form-data';
|
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||||
options.data = params;
|
axiosRequest.data = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('>>> Sending Request');
|
console.log('>>> Sending Request');
|
||||||
console.log(options);
|
console.log(axiosRequest);
|
||||||
|
|
||||||
// Todo: Choose based on platform (web/desktop)
|
// Todo: Choose based on platform (web/desktop)
|
||||||
sendHttpRequestInBrowser(options)
|
sendHttpRequestInBrowser(axiosRequest)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
||||||
// ipcRenderer
|
// ipcRenderer
|
||||||
// .invoke('send-http', options)
|
// .invoke('send-http', axiosRequest)
|
||||||
// .then(resolve)
|
// .then(resolve)
|
||||||
// .catch(reject);
|
// .catch(reject);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user