Add auto-download, fix curl pasting and code gen

This commit is contained in:
Gregory Schier 2017-02-01 12:21:14 -08:00
parent 883866a76a
commit 065a31a9a2
7 changed files with 153 additions and 36 deletions

View File

@ -167,7 +167,7 @@ describe('actuallySend()', () => {
);
expect(mock.basePath).toBe('http://localhost:80');
expect(response.error).toBe('');
expect(response.error).toBe(undefined);
expect(response.url).toBe('http://localhost/?foo%20bar=hello%26world');
expect(response.body).toBe(new Buffer('response body').toString('base64'));
expect(response.statusCode).toBe(200);
@ -208,7 +208,7 @@ describe('actuallySend()', () => {
);
expect(mock.basePath).toBe('http://localhost:80');
expect(response.error).toBe('');
expect(response.error).toBe(undefined);
expect(response.url).toBe('http://localhost/');
expect(response.body).toBe(new Buffer('response body').toString('base64'));
expect(response.statusCode).toBe(200);
@ -260,7 +260,7 @@ describe('actuallySend()', () => {
);
expect(mock.basePath).toBe('http://localhost:80');
expect(response.error).toBe('');
expect(response.error).toBe(undefined);
expect(response.url).toBe('http://localhost/');
expect(response.body).toBe(new Buffer('response body').toString('base64'));
expect(response.statusCode).toBe(200);

View File

@ -118,17 +118,15 @@ export function _buildRequestConfig (renderedRequest, patch = {}) {
}
export function _actuallySend (renderedRequest, workspace, settings, familyIndex = 0) {
return new Promise(async (resolve, reject) => {
return new Promise(async resolve => {
async function handleError (err, prefix = '') {
await models.response.create({
resolve({
url: renderedRequest.url,
parentId: renderedRequest._id,
elapsedTime: 0,
statusMessage: 'Error',
error: prefix ? `${prefix}: ${err.message}` : err.message
});
reject(err);
}
// Detect and set the proxy based on the request protocol
@ -215,10 +213,10 @@ export function _actuallySend (renderedRequest, workspace, settings, familyIndex
if (isNetworkRelatedError && nextFamilyIndex < FAMILY_FALLBACKS.length) {
const family = FAMILY_FALLBACKS[nextFamilyIndex];
console.log(`-- Falling back to family ${family} --`);
_actuallySend(
return _actuallySend(
renderedRequest, workspace, settings, nextFamilyIndex
).then(resolve, reject);
return;
}
let message = err.toString();
@ -227,14 +225,12 @@ export function _actuallySend (renderedRequest, workspace, settings, familyIndex
message += `Code: ${err.code}`;
}
await models.response.create({
return resolve({
url: config.url,
parentId: renderedRequest._id,
statusMessage: 'Error',
error: message
});
return reject(err);
}
// handle response headers
@ -272,7 +268,7 @@ export function _actuallySend (renderedRequest, workspace, settings, familyIndex
}
const bodyEncoding = 'base64';
const responsePatch = {
return resolve({
parentId: renderedRequest._id,
statusCode: networkResponse.statusCode,
statusMessage: networkResponse.statusMessage,
@ -283,9 +279,7 @@ export function _actuallySend (renderedRequest, workspace, settings, familyIndex
body: networkResponse.body.toString(bodyEncoding),
encoding: bodyEncoding,
headers: headers
};
models.response.create(responsePatch).then(resolve, reject);
});
};
const requestStartTime = Date.now();
@ -295,15 +289,13 @@ export function _actuallySend (renderedRequest, workspace, settings, familyIndex
cancelRequestFunction = async () => {
req.abort();
await models.response.create({
resolve({
url: config.url,
parentId: renderedRequest._id,
elapsedTime: Date.now() - requestStartTime,
statusMessage: 'Cancelled',
error: 'The request was cancelled'
});
return reject(new Error('Cancelled'));
}
})
}
@ -321,7 +313,7 @@ export async function send (requestId, environmentId) {
renderedRequest = await getRenderedRequest(request, environmentId);
} catch (e) {
// Failed to render. Must be the user's fault
return await models.response.create({
return resolve({
parentId: request._id,
statusCode: STATUS_CODE_RENDER_FAILED,
error: e.message
@ -333,5 +325,5 @@ export async function send (requestId, environmentId) {
const workspace = ancestors.find(doc => doc.type === models.workspace.type);
// Render succeeded so we're good to go!
return await _actuallySend(renderedRequest, workspace, settings);
return _actuallySend(renderedRequest, workspace, settings);
}

View File

@ -71,6 +71,7 @@ class RequestPane extends PureComponent {
editorKeyMap,
editorLineWrapping,
handleSend,
handleSendAndDownload,
forceRefreshCounter,
useBulkHeaderEditor,
handleGenerateCode,
@ -152,6 +153,7 @@ class RequestPane extends PureComponent {
onUrlPaste={importRequest}
handleGenerateCode={handleGenerateCode}
handleSend={handleSend}
handleSendAndDownload={handleSendAndDownload}
url={request.url}
/>
</header>
@ -279,6 +281,7 @@ class RequestPane extends PureComponent {
RequestPane.propTypes = {
// Functions
handleSend: PropTypes.func.isRequired,
handleSendAndDownload: PropTypes.func.isRequired,
handleCreateRequest: PropTypes.func.isRequired,
handleGenerateCode: PropTypes.func.isRequired,
updateRequest: PropTypes.func.isRequired,

View File

@ -1,23 +1,28 @@
import React, {Component, PropTypes} from 'react';
import classnames from 'classnames';
import {remote} from 'electron';
import {DEBOUNCE_MILLIS, isMac} from '../../common/constants';
import {Dropdown, DropdownButton, DropdownItem, DropdownDivider, DropdownHint} from './base/dropdown';
import {trackEvent} from '../../analytics';
import MethodDropdown from './dropdowns/MethodDropdown';
import PromptModal from './modals/PromptModal';
import {showModal} from './modals/index';
import PromptButton from './base/PromptButton';
class RequestUrlBar extends Component {
state = {
currentInterval: null,
currentTimeout: null,
downloadPath: null
};
_urlChangeDebounceTimeout = null;
_handleFormSubmit = e => {
e.preventDefault();
e.stopPropagation();
this.props.handleSend();
this._handleSend();
};
_handleMethodChange = method => {
@ -28,15 +33,28 @@ class RequestUrlBar extends Component {
_handleUrlChange = e => {
const url = e.target.value;
clearTimeout(this._timeout);
this._timeout = setTimeout(() => {
clearTimeout(this._urlChangeDebounceTimeout);
this._urlChangeDebounceTimeout = setTimeout(() => {
this.props.onUrlChange(url);
}, DEBOUNCE_MILLIS);
};
_handleUrlPaste = e => {
/*
* Prevent the change handler from being called. Note that this is in a timeout
* because we want it to happen after the onChange callback. If it happens before,
* then the change will overwrite anything that we do.
*
* Also, note that there is still a potential race condition here if, for some reason,
* the onChange callback is not called before DEBOUNCE_MILLIS is over. This is extremely
* unlikely since it should happen in the same tick.
*/
const text = e.clipboardData.getData('text/plain');
setTimeout(() => {
// Clear any update timeouts that may have happened since we started waiting
clearTimeout(this._urlChangeDebounceTimeout);
this.props.onUrlPaste(text);
}, DEBOUNCE_MILLIS);
};
_handleGenerateCode = () => {
@ -44,6 +62,27 @@ class RequestUrlBar extends Component {
trackEvent('Request', 'Generate Code', 'Send Action');
};
_handleSetDownloadLocation = () => {
const options = {
title: 'Select Download Location',
buttonLabel: 'Select',
properties: ['openDirectory'],
};
remote.dialog.showOpenDialog(options, paths => {
if (!paths || paths.length === 0) {
trackEvent('Response', 'Download Select Cancel');
return;
}
this.setState({downloadPath: paths[0]});
});
};
_handleClearDownloadLocation = () => {
this.setState({downloadPath: null});
};
_handleKeyDown = e => {
if (!this._input) {
return;
@ -63,7 +102,13 @@ class RequestUrlBar extends Component {
// XXX this._handleStopInterval(); XXX
this._handleStopTimeout();
const {downloadPath} = this.state;
if (downloadPath) {
this.props.handleSendAndDownload(downloadPath);
} else {
this.props.handleSend();
}
};
_handleSendAfterDelay = async () => {
@ -130,16 +175,14 @@ class RequestUrlBar extends Component {
componentDidMount () {
document.body.addEventListener('keydown', this._handleKeyDown);
document.body.addEventListener('keyup', this._handleKeyUp);
}
componentWillUnmount () {
document.body.removeEventListener('keydown', this._handleKeyDown);
document.body.removeEventListener('keyup', this._handleKeyUp);
}
renderSendButton () {
const {currentInterval, currentTimeout} = this.state;
const {currentInterval, currentTimeout, downloadPath} = this.state;
let cancelButton = null;
if (currentInterval) {
@ -169,7 +212,7 @@ class RequestUrlBar extends Component {
<DropdownButton className="urlbar__send-btn"
onClick={this._handleClickSend}
type="submit">
Send
{downloadPath ? "Download" : "Send"}
</DropdownButton>
<DropdownDivider>Basic</DropdownDivider>
<DropdownItem type="submit">
@ -186,6 +229,18 @@ class RequestUrlBar extends Component {
<DropdownItem onClick={this._handleSendOnInterval}>
<i className="fa fa-repeat"/> Repeat on Interval
</DropdownItem>
{downloadPath ? (
<DropdownItem stayOpenAfterClick={true}
buttonClass={PromptButton}
addIcon={true}
onClick={this._handleClearDownloadLocation}>
<i className="fa fa-stop-circle"/> Stop Auto-Download
</DropdownItem>
) : (
<DropdownItem onClick={this._handleSetDownloadLocation}>
<i className="fa fa-download"/> Download After Send
</DropdownItem>
)}
</Dropdown>
)
}
@ -220,6 +275,7 @@ class RequestUrlBar extends Component {
RequestUrlBar.propTypes = {
handleSend: PropTypes.func.isRequired,
handleSendAndDownload: PropTypes.func.isRequired,
onUrlChange: PropTypes.func.isRequired,
onUrlPaste: PropTypes.func.isRequired,
onMethodChange: PropTypes.func.isRequired,

View File

@ -119,6 +119,13 @@ class Wrapper extends Component {
handleSendRequestWithEnvironment(activeRequestId, activeEnvironmentId);
};
_handleSendAndDownloadRequestWithActiveEnvironment = filename => {
const {activeRequest, activeEnvironment, handleSendAndDownloadRequestWithEnvironment} = this.props;
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : 'n/a';
handleSendAndDownloadRequestWithEnvironment(activeRequestId, activeEnvironmentId, filename);
};
_handleSetPreviewMode = previewMode => {
const activeRequest = this.props.activeRequest;
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
@ -162,6 +169,7 @@ class Wrapper extends Component {
handleStartDragPane,
handleStartDragSidebar,
handleSetSidebarFilter,
handleGenerateCodeForActiveRequest,
handleGenerateCode,
isLoading,
loadStartTime,
@ -234,7 +242,7 @@ class Wrapper extends Component {
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
workspace={activeWorkspace}
handleCreateRequest={handleCreateRequestForWorkspace}
handleGenerateCode={handleGenerateCode}
handleGenerateCode={handleGenerateCodeForActiveRequest}
updateRequest={this._handleUpdateRequest}
updateRequestBody={this._handleUpdateRequestBody}
updateRequestUrl={this._handleUpdateRequestUrl}
@ -248,6 +256,7 @@ class Wrapper extends Component {
updateSettingsUseBulkHeaderEditor={this._handleUpdateSettingsUseBulkHeaderEditor}
forceRefreshCounter={this.state.forceRefreshRequestPaneCounter}
handleSend={this._handleSendRequestWithActiveEnvironment}
handleSendAndDownload={this._handleSendAndDownloadRequestWithActiveEnvironment}
/>
<div className="drag drag--pane">
@ -339,6 +348,7 @@ Wrapper.propTypes = {
handleDuplicateRequest: PropTypes.func.isRequired,
handleDuplicateRequestGroup: PropTypes.func.isRequired,
handleCreateRequestGroup: PropTypes.func.isRequired,
handleGenerateCodeForActiveRequest: PropTypes.func.isRequired,
handleGenerateCode: PropTypes.func.isRequired,
handleCreateRequestForWorkspace: PropTypes.func.isRequired,
handleSetRequestPaneRef: PropTypes.func.isRequired,
@ -353,6 +363,7 @@ Wrapper.propTypes = {
handleResetDragPane: PropTypes.func.isRequired,
handleSetRequestGroupCollapsed: PropTypes.func.isRequired,
handleSendRequestWithEnvironment: PropTypes.func.isRequired,
handleSendAndDownloadRequestWithEnvironment: PropTypes.func.isRequired,
// Properties
loadStartTime: PropTypes.number.isRequired,

View File

@ -66,7 +66,7 @@ class PromptButton extends Component {
if (state === STATE_ASK && addIcon) {
innerMsg = (
<span className="danger">
<i className="fa fa-exclamation-circle"></i> {CONFIRM_MESSAGE}
<i className="fa fa-exclamation-circle"/> {CONFIRM_MESSAGE}
</span>
)
} else if (state === STATE_ASK) {

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react';
import fs from 'fs';
import {ipcRenderer} from 'electron';
import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
@ -25,6 +26,8 @@ import GenerateCodeModal from '../components/modals/GenerateCodeModal';
import WorkspaceSettingsModal from '../components/modals/WorkspaceSettingsModal';
import * as network from '../../common/network';
import {debounce} from '../../common/misc';
import * as mime from 'mime-types';
import * as path from 'path';
const KEY_ENTER = 13;
const KEY_COMMA = 188;
@ -158,8 +161,12 @@ class App extends Component {
this._handleSetActiveRequest(newRequest._id)
};
_handleGenerateCode = () => {
showModal(GenerateCodeModal, this.props.activeRequest);
_handleGenerateCodeForActiveRequest = () => {
this._handleGenerateCode(this.props.activeRequest);
};
_handleGenerateCode = request => {
showModal(GenerateCodeModal, request);
};
_updateRequestGroupMetaByParentId = async (requestGroupId, patch) => {
@ -233,6 +240,51 @@ class App extends Component {
this._updateRequestMetaByParentId(requestId, {responseFilter});
};
_handleSendAndDownloadRequestWithEnvironment = async (requestId, environmentId, dir) => {
const request = await models.request.getById(requestId);
if (!request) {
return;
}
// NOTE: Since request is by far the most popular event, we will throttle
// it so that we only track it if the request has changed since the last one
const key = request._id;
if (this._sendRequestTrackingKey !== key) {
trackEvent('Request', 'Send and Download');
trackLegacyEvent('Request Send');
this._sendRequestTrackingKey = key;
}
// Start loading
this.props.handleStartLoading(requestId);
try {
const responsePatch = await network.send(requestId, environmentId);
if (responsePatch.statusCode >= 200 && responsePatch.statusCode < 300) {
const extension = mime.extension(responsePatch.contentType) || '';
const name = request.name.replace(/\s/g, '-').toLowerCase();
const filename = path.join(dir, `${name}.${extension}`);
const partialResponse = Object.assign({}, responsePatch, {
contentType: "text/plain",
body: `Saved to ${filename}`,
encoding: 'utf8',
});
await models.response.create(partialResponse);
fs.writeFile(filename, responsePatch.body, responsePatch.encoding)
} else {
await models.response.create(responsePatch);
}
} catch (e) {
// It's OK
}
// Unset active response because we just made a new one
await this._updateRequestMetaByParentId(requestId, {activeResponseId: null});
// Stop loading
this.props.handleStopLoading(requestId);
};
_handleSendRequestWithEnvironment = async (requestId, environmentId) => {
const request = await models.request.getById(requestId);
if (!request) {
@ -251,7 +303,8 @@ class App extends Component {
this.props.handleStartLoading(requestId);
try {
await network.send(requestId, environmentId);
const responsePatch = await network.send(requestId, environmentId);
await models.response.create(responsePatch);
} catch (e) {
// It's OK
}
@ -450,9 +503,11 @@ class App extends Component {
handleDuplicateRequestGroup={this._requestGroupDuplicate}
handleCreateRequestGroup={this._requestGroupCreate}
handleGenerateCode={this._handleGenerateCode}
handleGenerateCodeForActiveRequest={this._handleGenerateCodeForActiveRequest}
handleSetResponsePreviewMode={this._handleSetResponsePreviewMode}
handleSetResponseFilter={this._handleSetResponseFilter}
handleSendRequestWithEnvironment={this._handleSendRequestWithEnvironment}
handleSendAndDownloadRequestWithEnvironment={this._handleSendAndDownloadRequestWithEnvironment}
handleSetActiveResponse={this._handleSetActiveResponse}
handleSetActiveRequest={this._handleSetActiveRequest}
handleSetActiveEnvironment={this._handleSetActiveEnvironment}