feat: cancel running request (resolves #26)

This commit is contained in:
Anoop M D 2022-10-14 01:34:15 +05:30
parent 097a6240ad
commit 410bc70318
8 changed files with 118 additions and 47 deletions

View File

@ -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">

View File

@ -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>

View File

@ -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;

View File

@ -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">

View File

@ -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,
@ -108,3 +114,38 @@ export const deleteCollection = (collectionUid) => (dispatch, getState) => {
.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
}))
}
};

View File

@ -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);

View 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];
}

View File

@ -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);
}); });