diff --git a/app/common/constants.js b/app/common/constants.js index 59d5a8aee7..8054a523c8 100644 --- a/app/common/constants.js +++ b/app/common/constants.js @@ -116,22 +116,19 @@ export const CONTENT_TYPE_XML = 'application/xml'; export const CONTENT_TYPE_TEXT = 'text/plain'; export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'; export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; -export const CONTENT_TYPE_OTHER = ''; +export const CONTENT_TYPE_FILE = 'application/octet-stream'; +export const CONTENT_TYPE_RAW = ''; export const contentTypesMap = { [CONTENT_TYPE_JSON]: 'JSON', [CONTENT_TYPE_XML]: 'XML', // [CONTENT_TYPE_FORM_DATA]: 'Form Data', - [CONTENT_TYPE_FORM_URLENCODED]: 'Url Encoded', + [CONTENT_TYPE_FORM_URLENCODED]: 'Form Url Encoded', [CONTENT_TYPE_TEXT]: 'Plain Text', - [CONTENT_TYPE_OTHER]: 'Other', + [CONTENT_TYPE_FILE]: 'File Upload', + [CONTENT_TYPE_RAW]: 'Raw Body', }; -export const BODY_TYPE_RAW = 'raw'; -export const BODY_TYPE_FILE = 'file'; -export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'; -export const BODY_TYPE_FORM = 'multipart/form-data'; - /** * Get the friendly name for a given content type * @@ -139,7 +136,7 @@ export const BODY_TYPE_FORM = 'multipart/form-data'; * @returns {*|string} */ export function getContentTypeName (contentType) { - return contentTypesMap[contentType] || contentTypesMap[CONTENT_TYPE_OTHER]; + return contentTypesMap[contentType] || 'Other'; } export function getContentTypeFromHeaders (headers) { diff --git a/app/common/har.js b/app/common/har.js index 00ef839a2f..ff189a0e3a 100644 --- a/app/common/har.js +++ b/app/common/har.js @@ -1,8 +1,11 @@ +import fs from 'fs'; import * as models from '../models'; import {getRenderedRequest} from './render'; import {jarFromCookies} from './cookies'; import * as util from './misc'; import * as misc from './misc'; +import {CONTENT_TYPE_FILE} from './constants'; +import {newBodyRaw} from '../models/request'; export function exportHarWithRequest (renderedRequest, addContentLength = false) { if (addContentLength) { @@ -18,8 +21,17 @@ export function exportHarWithRequest (renderedRequest, addContentLength = false) } } - // Luckily, Insomnia uses the same body format as HAR :) - const postData = renderedRequest.body; + let postData = ''; + if (renderedRequest.body.fileName) { + try { + postData = newBodyRaw(fs.readFileSync(renderedRequest.body.fileName, 'base64')); + } catch (e) { + console.warn('[code gen] Failed to read file', e); + } + } else { + // For every other type, Insomnia uses the same body format as HAR + postData = renderedRequest.body; + } return { method: renderedRequest.method, diff --git a/app/common/misc.js b/app/common/misc.js index 6c4236fdce..5d6e6b5d1f 100644 --- a/app/common/misc.js +++ b/app/common/misc.js @@ -154,3 +154,25 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) { callback.apply(null, results['__key__']) }, millis).bind(null, '__key__'); } + +export function describeByteSize (bytes) { + bytes = Math.round(bytes * 10) / 10; + let size; + + let unit = 'B'; + if (bytes < 1024) { + size = bytes; + unit = 'B'; + } else if (bytes < 1024 * 1024) { + size = bytes / 1024; + unit = 'KB'; + } else if (bytes < 1024 * 1024) { + size = bytes / 1024 / 1024; + unit = 'MB'; + } else { + size = bytes / 1024 / 1024 / 1024; + unit = 'GB'; + } + + return Math.round(size * 10) / 10 + ' ' + unit; +} diff --git a/app/common/network.js b/app/common/network.js index d2ab69d3df..8a9405714c 100644 --- a/app/common/network.js +++ b/app/common/network.js @@ -9,6 +9,8 @@ import {jarFromCookies, cookiesFromJar, cookieHeaderValueForUri} from './cookies import {setDefaultProtocol} from './misc'; import {getRenderedRequest} from './render'; import {swapHost} from './dns'; +import {CONTENT_TYPE_FILE} from './constants'; +import * as fs from 'fs'; let cancelRequestFunction = null; @@ -50,6 +52,8 @@ export function _buildRequestConfig (renderedRequest, patch = {}) { config.body = buildFromParams(renderedRequest.body.params || [], true); } else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) { // TODO: This + } else if (renderedRequest.body.mimeType === CONTENT_TYPE_FILE) { + config.body = fs.readFileSync(renderedRequest.body.fileName); } else { config.body = renderedRequest.body.text || ''; } @@ -85,14 +89,25 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) { const proxyHost = protocol === 'https:' ? httpsProxy : httpProxy; const proxy = proxyHost ? setDefaultProtocol(proxyHost) : null; - const config = _buildRequestConfig(renderedRequest, { - jar: null, // We're doing our own cookies - proxy: proxy, - followAllRedirects: settings.followRedirects, - followRedirect: settings.followRedirects, - timeout: settings.timeout > 0 ? settings.timeout : null, - rejectUnauthorized: settings.validateSSL - }, true); + let config; + try { + config = _buildRequestConfig(renderedRequest, { + jar: null, // We're doing our own cookies + proxy: proxy, + followAllRedirects: settings.followRedirects, + followRedirect: settings.followRedirects, + timeout: settings.timeout > 0 ? settings.timeout : null, + rejectUnauthorized: settings.validateSSL + }, true); + } catch (e) { + const response = await models.response.create({ + parentId: renderedRequest._id, + elapsedTime: 0, + statusMessage: 'Error', + error: e.message + }); + return resolve(response); + } // Add the cookie header to the request const cookieJar = renderedRequest.cookieJar; diff --git a/app/models/request.js b/app/models/request.js index a5039f821a..6bf485d783 100644 --- a/app/models/request.js +++ b/app/models/request.js @@ -3,6 +3,9 @@ import * as db from '../common/database'; import {getContentTypeHeader} from '../common/misc'; import {deconstructToParams} from '../common/querystring'; import {CONTENT_TYPE_JSON} from '../common/constants'; +import {CONTENT_TYPE_XML} from '../common/constants'; +import {CONTENT_TYPE_FILE} from '../common/constants'; +import {CONTENT_TYPE_TEXT} from '../common/constants'; export const name = 'Request'; export const type = 'Request'; @@ -22,6 +25,24 @@ export function init () { }; } +export function getBodyDescription (body) { + if (body.fileName) { + return 'File Upload'; + } else if (body.mimeType === CONTENT_TYPE_FORM_URLENCODED) { + return 'Form Url Encoded'; + } else if (body.mimeType === CONTENT_TYPE_FORM_DATA) { + return 'Form Data'; + } else if (body.mimeType === CONTENT_TYPE_JSON) { + return 'JSON'; + } else if (body.mimeType === CONTENT_TYPE_XML) { + return 'XML'; + } else if (body.mimeType === CONTENT_TYPE_TEXT) { + return 'Plain Text'; + } else { + return 'Raw Body'; + } +} + export function newBodyRaw (rawBody, contentType) { if (!contentType) { return {text: rawBody}; @@ -38,6 +59,13 @@ export function newBodyFormUrlEncoded (parameters) { } } +export function newBodyFile (path) { + return { + mimeType: CONTENT_TYPE_FILE, + fileName: path + } +} + export function newBodyForm (parameters) { return { mimeType: CONTENT_TYPE_FORM_DATA, @@ -99,6 +127,8 @@ export function updateMimeType (request, mimeType) { request.body = newBodyForm(request.body.params || []); } else if (mimeType === CONTENT_TYPE_JSON) { request.body = newBodyRaw(request.body.text || ''); + } else if (mimeType === CONTENT_TYPE_FILE) { + request.body = newBodyFile(''); } else { request.body = newBodyRaw(request.body.text || '', mimeType); } diff --git a/app/ui/components/RequestPane.js b/app/ui/components/RequestPane.js index 598265b621..7784496d3a 100644 --- a/app/ui/components/RequestPane.js +++ b/app/ui/components/RequestPane.js @@ -9,6 +9,7 @@ import AuthEditor from './editors/AuthEditor'; import RequestUrlBar from './RequestUrlBar.js'; import {MOD_SYM, getContentTypeName, getContentTypeFromHeaders} from '../../common/constants'; import {debounce} from '../../common/misc'; +import {getBodyDescription} from '../../models/request'; class RequestPane extends Component { render () { diff --git a/app/ui/components/base/FileInputButton.js b/app/ui/components/base/FileInputButton.js new file mode 100644 index 0000000000..13e27ee3ed --- /dev/null +++ b/app/ui/components/base/FileInputButton.js @@ -0,0 +1,55 @@ +import React, {Component, PropTypes} from 'react'; +import {remote} from 'electron'; +import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown'; +import PromptButton from '../base/PromptButton'; + +class FileInputButton extends Component { + _handleUnsetFile () { + this.props.onChange(''); + } + + _handleChooseFile () { + const options = { + title: 'Import File', + buttonLabel: 'Import', + properties: ['openFile'] + }; + + remote.dialog.showOpenDialog(options, async paths => { + if (!paths || paths.length === 0) { + return; + } + + const path = paths[0]; + this.props.onChange(path); + }) + } + + render () { + const {className} = this.props; + return ( + + + Choose File + + this._handleChooseFile()}> + + Choose File + + this._handleUnsetFile()}> + + Unset + + + ) + } +} + +FileInputButton.propTypes = { + onChange: PropTypes.func.isRequired, + path: PropTypes.string.isRequired, +}; + +export default FileInputButton; diff --git a/app/ui/components/editors/body/BodyEditor.js b/app/ui/components/editors/body/BodyEditor.js index 6bd318e606..0a9ac3b985 100644 --- a/app/ui/components/editors/body/BodyEditor.js +++ b/app/ui/components/editors/body/BodyEditor.js @@ -2,10 +2,11 @@ import React, {PropTypes, Component} from 'react'; import RawEditor from './RawEditor'; import UrlEncodedEditor from './UrlEncodedEditor'; import FormEditor from './FormEditor'; -import {getContentTypeFromHeaders, BODY_TYPE_FORM_URLENCODED, BODY_TYPE_FORM, BODY_TYPE_FILE} from '../../../../common/constants'; +import FileEditor from './FileEditor'; +import {getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA} from '../../../../common/constants'; import {newBodyRaw, newBodyFormUrlEncoded, newBodyForm} from '../../../../models/request'; -import {CONTENT_TYPE_FORM_URLENCODED} from '../../../../common/constants'; -import {CONTENT_TYPE_FORM_DATA} from '../../../../common/constants'; +import {CONTENT_TYPE_FILE} from '../../../../common/constants'; +import {newBodyFile} from '../../../../models/request'; class BodyEditor extends Component { constructor (props) { @@ -13,6 +14,7 @@ class BodyEditor extends Component { this._boundHandleRawChange = this._handleRawChange.bind(this); this._boundHandleFormUrlEncodedChange = this._handleFormUrlEncodedChange.bind(this); this._boundHandleFormChange = this._handleFormChange.bind(this); + this._boundHandleFileChange = this._handleFileChange.bind(this); } _handleRawChange (rawValue) { @@ -36,6 +38,12 @@ class BodyEditor extends Component { onChange(newBody); } + _handleFileChange (path) { + const {onChange} = this.props; + const newBody = newBodyFile(path); + onChange(newBody); + } + render () { const {fontSize, lineWrapping, request} = this.props; const bodyType = request.body.mimeType; @@ -57,9 +65,14 @@ class BodyEditor extends Component { parameters={request.body.params || []} /> ) - } else if (bodyType === BODY_TYPE_FILE) { - // TODO - return null + } else if (bodyType === CONTENT_TYPE_FILE) { + return ( + + ) } else { const contentType = getContentTypeFromHeaders(request.headers); return ( diff --git a/app/ui/components/editors/body/FileEditor.js b/app/ui/components/editors/body/FileEditor.js index e69de29bb2..ca10f794d4 100644 --- a/app/ui/components/editors/body/FileEditor.js +++ b/app/ui/components/editors/body/FileEditor.js @@ -0,0 +1,53 @@ +import fs from 'fs'; +import electron from 'electron'; +import React, {PropTypes, Component} from 'react'; +import FileInputButton from '../../base/FileInputButton'; +import * as misc from '../../../../common/misc'; + +class FileEditor extends Component { + render () { + const {path, onChange} = this.props; + + // Replace home path with ~/ to make the path shorter + const homeDirectory = electron.remote.app.getPath('home'); + const pathDescription = path.replace(homeDirectory, '~'); + + let sizeDescription = ''; + try { + const bytes = fs.statSync(path).size; + sizeDescription = misc.describeByteSize(bytes); + } catch (e) { + sizeDescription = ''; + } + + return ( +
+

+ {path ? ( + + + {pathDescription} + + {" "} + ({sizeDescription}) + + ) : ( + No file selected + )} +

+ +
+ ) + } +} + +FileEditor.propTypes = { + onChange: PropTypes.func.isRequired, + path: PropTypes.string.isRequired, +}; + +export default FileEditor; diff --git a/app/ui/components/tags/SizeTag.js b/app/ui/components/tags/SizeTag.js index c768d47e76..6316906eb0 100644 --- a/app/ui/components/tags/SizeTag.js +++ b/app/ui/components/tags/SizeTag.js @@ -1,25 +1,8 @@ import React, {PropTypes} from 'react'; +import * as misc from '../../../common/misc'; const SizeTag = props => { - const bytes = Math.round(props.bytes * 10) / 10; - let size; - - let unit = 'B'; - if (bytes < 1024) { - size = bytes; - unit = 'B'; - } else if (bytes < 1024 * 1024) { - size = bytes / 1024; - unit = 'KB'; - } else if (bytes < 1024 * 1024) { - size = bytes / 1024 / 1024; - unit = 'MB'; - } else { - size = bytes / 1024 / 1024 / 1024; - unit = 'GB'; - } - - const responseSizeString = Math.round(size * 10) / 10 + ' ' + unit; + const responseSizeString = misc.describeByteSize(props.bytes); return (
diff --git a/app/ui/css/components/forms.less b/app/ui/css/components/forms.less index 17eb3bd03d..a2fd71d770 100644 --- a/app/ui/css/components/forms.less +++ b/app/ui/css/components/forms.less @@ -87,20 +87,30 @@ label > .form-control, } } -button:disabled { +.btn:disabled { opacity: 0.4; } -button:focus:not(:disabled), -button.focus:not(:disabled), -button:hover:not(:disabled) { +.btn:focus:not(:disabled), +.btn.focus:not(:disabled), +.btn:hover:not(:disabled) { background: @hl-xs; } -button:active:not(:disabled) { +.btn:active:not(:disabled) { background: @hl-md; } +.btn.btn--no-background { + opacity: 0.5; + background: transparent; + + &:hover { + opacity: 1; + background: transparent; + } +} + textarea, input, button { box-sizing: border-box; text-align: left;