mirror of
https://github.com/Kong/insomnia
synced 2024-11-12 17:26:32 +00:00
7adf8591c1
* Almost working * First working implementation * Simplified analytics utils * Fix tests
294 lines
7.9 KiB
JavaScript
294 lines
7.9 KiB
JavaScript
// @flow
|
|
import * as React from 'react';
|
|
import * as electron from 'electron';
|
|
import mimes from 'mime-types';
|
|
import fs from 'fs';
|
|
import moment from 'moment';
|
|
import path from 'path';
|
|
import {PassThrough} from 'stream';
|
|
import multiparty from 'multiparty';
|
|
import autobind from 'autobind-decorator';
|
|
import ResponseViewer from './response-viewer';
|
|
import {getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY} from '../../../common/constants';
|
|
import type {ResponseHeader} from '../../../models/response';
|
|
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown/index';
|
|
import {trackEvent} from '../../../common/analytics';
|
|
import WrapperModal from '../modals/wrapper-modal';
|
|
import {showModal} from '../modals/index';
|
|
import ResponseHeadersViewer from './response-headers-viewer';
|
|
|
|
type Part = {
|
|
name: string,
|
|
bytes: number,
|
|
value: Buffer,
|
|
filename: string | null,
|
|
headers: Array<ResponseHeader>,
|
|
};
|
|
|
|
type Props = {
|
|
responseId: string,
|
|
bodyBuffer: Buffer | null,
|
|
contentType: string,
|
|
filter: string,
|
|
filterHistory: Array<string>,
|
|
editorFontSize: number,
|
|
editorIndentSize: number,
|
|
editorKeyMap: string,
|
|
editorLineWrapping: boolean,
|
|
url: string
|
|
};
|
|
|
|
type State = {
|
|
activePart: number,
|
|
parts: Array<Part>,
|
|
error: string | null
|
|
};
|
|
|
|
@autobind
|
|
class ResponseMultipart extends React.PureComponent<Props, State> {
|
|
constructor (props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
activePart: -1,
|
|
parts: [],
|
|
error: null
|
|
};
|
|
}
|
|
|
|
componentDidMount () {
|
|
this._setParts();
|
|
}
|
|
|
|
async _setParts () {
|
|
try {
|
|
const parts = await this._getParts();
|
|
this.setState({parts, activePart: 0, error: null});
|
|
} catch (err) {
|
|
this.setState({error: err.message});
|
|
}
|
|
}
|
|
|
|
_describePart (part: Part) {
|
|
const segments = [part.name];
|
|
if (part.filename) {
|
|
segments.push(`(${part.filename})`);
|
|
}
|
|
|
|
return segments.join(' ');
|
|
}
|
|
|
|
async _handleSelectPart (index: number) {
|
|
this.setState({activePart: index});
|
|
}
|
|
|
|
_getBody () {
|
|
const {parts, activePart} = this.state;
|
|
const part = parts[activePart];
|
|
|
|
if (!part) {
|
|
return new Buffer('');
|
|
}
|
|
|
|
return part.value;
|
|
}
|
|
|
|
_handleViewHeaders () {
|
|
const {parts, activePart} = this.state;
|
|
const part = parts[activePart];
|
|
|
|
if (!part) {
|
|
return;
|
|
}
|
|
|
|
showModal(WrapperModal, {
|
|
title: <span>Headers for <code>{part.name}</code></span>,
|
|
body: <ResponseHeadersViewer
|
|
headers={[...part.headers, ...part.headers, ...part.headers, ...part.headers]}/>
|
|
});
|
|
}
|
|
|
|
_handleSaveAsFile () {
|
|
const {parts, activePart} = this.state;
|
|
const part = parts[activePart];
|
|
|
|
if (!part) {
|
|
return;
|
|
}
|
|
|
|
const contentType = getContentTypeFromHeaders(part.headers, 'text/plain');
|
|
|
|
const extension = mimes.extension(contentType) || '.txt';
|
|
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
|
|
const dir = lastDir || electron.remote.app.getPath('desktop');
|
|
const date = moment().format('YYYY-MM-DD');
|
|
const filename = part.filename || `${part.name}_${date}`;
|
|
const options = {
|
|
title: 'Save as File',
|
|
buttonLabel: 'Save',
|
|
defaultPath: path.join(dir, filename),
|
|
filters: [{
|
|
name: 'Download', extensions: [extension]
|
|
}]
|
|
};
|
|
|
|
electron.remote.dialog.showSaveDialog(options, outputPath => {
|
|
if (!outputPath) {
|
|
trackEvent('Response', 'Multipart Save Cancel');
|
|
return;
|
|
}
|
|
|
|
// Remember last exported path
|
|
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
|
|
|
|
// Save the file
|
|
fs.writeFile(outputPath, part.value, err => {
|
|
if (err) {
|
|
console.warn('Failed to save multipart to file', err);
|
|
trackEvent('Response', 'Multipart Save Failure');
|
|
} else {
|
|
trackEvent('Response', 'Multipart Save Success');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
_getParts (): Promise<Array<Part>> {
|
|
return new Promise((resolve, reject) => {
|
|
const {bodyBuffer, contentType} = this.props;
|
|
const parts = [];
|
|
|
|
if (!bodyBuffer) {
|
|
return resolve(parts);
|
|
}
|
|
|
|
const fakeReq = new PassThrough();
|
|
(fakeReq: Object).headers = {
|
|
'content-type': contentType
|
|
};
|
|
|
|
const form = new multiparty.Form();
|
|
form.on('part', part => {
|
|
const dataBuffers = [];
|
|
part.on('data', data => {
|
|
dataBuffers.push(data);
|
|
});
|
|
|
|
part.on('error', err => {
|
|
reject(new Error(`Failed to parse part: ${err.message}`));
|
|
});
|
|
|
|
part.on('end', () => {
|
|
parts.push({
|
|
value: Buffer.concat(dataBuffers),
|
|
name: part.name,
|
|
filename: part.filename || null,
|
|
bytes: part.byteCount,
|
|
headers: Object.keys(part.headers).map(name => ({name, value: part.headers[name]}))
|
|
});
|
|
});
|
|
});
|
|
|
|
form.on('error', err => {
|
|
reject(err);
|
|
});
|
|
|
|
form.on('close', () => {
|
|
resolve(parts);
|
|
});
|
|
|
|
form.parse(fakeReq);
|
|
|
|
fakeReq.write(bodyBuffer);
|
|
fakeReq.end();
|
|
});
|
|
}
|
|
|
|
render () {
|
|
const {
|
|
responseId,
|
|
filter,
|
|
filterHistory,
|
|
url,
|
|
editorFontSize,
|
|
editorIndentSize,
|
|
editorKeyMap,
|
|
editorLineWrapping
|
|
} = this.props;
|
|
|
|
const {
|
|
activePart,
|
|
parts,
|
|
error
|
|
} = this.state;
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="pad monospace" style={{fontSize: editorFontSize}}>
|
|
Failed to parse multipart response: {error}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const selectedPart = parts[activePart];
|
|
|
|
return (
|
|
<div className="pad-sm tall wide"
|
|
style={{display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr)'}}>
|
|
<div className="pad-bottom-sm"
|
|
style={{display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto'}}>
|
|
<div>
|
|
<Dropdown wide>
|
|
<DropdownButton className="btn btn--clicky">
|
|
<div style={{minWidth: '200px', display: 'inline-block'}}>
|
|
{selectedPart ? this._describePart(selectedPart) : 'Unknown'}
|
|
</div>
|
|
<i className="fa fa-caret-down fa--skinny space-left"/>
|
|
</DropdownButton>
|
|
{parts.map((part, i) => (
|
|
<DropdownItem key={i} value={i} onClick={this._handleSelectPart}>
|
|
{i === activePart ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}
|
|
{this._describePart(part)}
|
|
</DropdownItem>
|
|
))}
|
|
</Dropdown>
|
|
</div>
|
|
<Dropdown right>
|
|
<DropdownButton className="btn btn--clicky">
|
|
<i className="fa fa-bars"/>
|
|
</DropdownButton>
|
|
<DropdownItem onClick={this._handleViewHeaders}>
|
|
<i className="fa fa-list"/> View Headers
|
|
</DropdownItem>
|
|
<DropdownItem onClick={this._handleSaveAsFile}>
|
|
<i className="fa fa-save"/> Save as File
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
</div>
|
|
{selectedPart ? (
|
|
<div className="tall wide">
|
|
<ResponseViewer
|
|
key={`${responseId}::${activePart}`}
|
|
getBody={this._getBody}
|
|
responseId={`${responseId}[${activePart}]`}
|
|
previewMode={PREVIEW_MODE_FRIENDLY}
|
|
filter={filter}
|
|
filterHistory={filterHistory}
|
|
editorFontSize={editorFontSize}
|
|
editorIndentSize={editorIndentSize}
|
|
editorKeyMap={editorKeyMap}
|
|
editorLineWrapping={editorLineWrapping}
|
|
url={url}
|
|
bytes={selectedPart.bytes || 0}
|
|
contentType={getContentTypeFromHeaders(selectedPart.headers, 'text/plain')}
|
|
updateFilter={null}
|
|
error={null}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default ResponseMultipart;
|