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

View File

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

View File

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

View File

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

View File

@ -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,
@ -107,4 +113,39 @@ export const deleteCollection = (collectionUid) => (dispatch, getState) => {
.then(resolve)
.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 { 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);

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