From 8a26fdbaa7d4fa78c5b1a03abcb7b62549300ab8 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Thu, 7 Jul 2022 14:28:49 +0200 Subject: [PATCH] multipart viewer class->fc (#4930) * multipart * self review * Apply suggestions from code review Co-authored-by: James Gatz Co-authored-by: James Gatz --- .../viewers/response-multipart-viewer.tsx | 394 ++++++++---------- 1 file changed, 176 insertions(+), 218 deletions(-) diff --git a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx index cbda4747f..805bfada3 100644 --- a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx @@ -1,15 +1,13 @@ -import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { format } from 'date-fns'; import { SaveDialogOptions } from 'electron'; import fs from 'fs'; import { extension as mimeExtension } from 'mime-types'; import multiparty from 'multiparty'; import path from 'path'; -import React, { PureComponent } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { PassThrough } from 'stream'; import { - AUTOBIND_CFG, getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY, } from '../../../common/constants'; @@ -20,9 +18,11 @@ import { DropdownItem } from '../base/dropdown/dropdown-item'; import { showModal } from '../modals/index'; import { WrapperModal } from '../modals/wrapper-modal'; import { ResponseHeadersViewer } from './response-headers-viewer'; -import { ResponseViewer } from './response-viewer'; +import { ResponseViewer } from './response-viewer'; interface Part { + id: number; + title: string; name: string; bytes: number; value: Buffer; @@ -43,98 +43,65 @@ interface Props { url: string; } -interface State { - activePart: number; - parts: Part[]; - error: string | null; -} +export const ResponseMultipartViewer: FC = ({ + download, + disableHtmlPreviewJs, + disablePreviewLinks, + editorFontSize, + filter, + filterHistory, + responseId, + url, + bodyBuffer, + contentType, +}) => { + const [parts, setParts] = useState([]); + const [selectedPart, setSelectedPart] = useState(); + const [error, setError] = useState(null); -@autoBindMethodsForReact(AUTOBIND_CFG) -export class ResponseMultipartViewer extends PureComponent { - state: State = { - activePart: -1, - parts: [], - error: null, - }; + useEffect(() => { + const init = async () => { + try { + const parts = await multipartBufferToArray({ bodyBuffer, contentType }); + setParts(parts); + setSelectedPart(parts[0]); + } catch (err) { + setError(err.message); + } + }; + init(); + }, [bodyBuffer, contentType]); - componentDidMount() { - this._setParts(); - } + const selectPart = useCallback((part: Part) => { + setSelectedPart(part); + }, []); - async _setParts() { - try { - const parts = await this._getParts(); - this.setState({ - parts, - activePart: 0, - error: null, - }); - } catch (err) { - this.setState({ - error: err.message, - }); - } - } + const partBuffer = useCallback(() => selectedPart?.value, [selectedPart]); - _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 Buffer.from(''); - } - - return part.value; - } - - _handleViewHeaders() { - const { parts, activePart } = this.state; - const part = parts[activePart]; - - if (!part) { + const viewHeaders = useCallback(() => { + if (!selectedPart) { return; } - showModal(WrapperModal, { title: ( - Headers for {part.name} + Headers for {selectedPart.name} ), - body: , + body: , }); - } + }, [selectedPart]); - async _handleSaveAsFile() { - const { parts, activePart } = this.state; - const part = parts[activePart]; - - if (!part) { + const saveAsFile = useCallback(async () => { + if (!selectedPart) { return; } - - const contentType = getContentTypeFromHeaders(part.headers, 'text/plain'); + const contentType = getContentTypeFromHeaders(selectedPart.headers, 'text/plain'); const extension = mimeExtension(contentType) || '.txt'; const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); const dir = lastDir || window.app.getPath('desktop'); const date = format(Date.now(), 'yyyy-MM-dd'); - const filename = part.filename || `${part.name}_${date}`; + const filename = selectedPart.filename || `${selectedPart.name}_${date}`; const options: SaveDialogOptions = { title: 'Save as File', buttonLabel: 'Save', @@ -158,157 +125,148 @@ export class ResponseMultipartViewer extends PureComponent { // Save the file try { // @ts-expect-error -- TSCONVERSION if filePath is undefined, don't try to write anything - await fs.promises.writeFile(filePath, part.value); + await fs.promises.writeFile(filePath, selectedPart.value); } catch (err) { console.warn('Failed to save multipart to file', err); } - } + }, [selectedPart]); - _getParts(): Promise { - return new Promise((resolve, reject) => { - const { bodyBuffer, contentType } = this.props; - const parts: Part[] = []; - - if (!bodyBuffer) { - return resolve(parts); - } - - const fakeReq = new PassThrough(); - // @ts-expect-error -- TSCONVERSION investigate `stream` types - fakeReq.headers = { - 'content-type': contentType, - }; - const form = new multiparty.Form(); - form.on('part', part => { - const dataBuffers: any[] = []; - 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); - }); - // @ts-expect-error -- TSCONVERSION - form.parse(fakeReq); - fakeReq.write(bodyBuffer); - fakeReq.end(); - }); - } - - render() { - const { - download, - disableHtmlPreviewJs, - disablePreviewLinks, - editorFontSize, - filter, - filterHistory, - responseId, - url, - } = this.props; - const { activePart, parts, error } = this.state; - - if (error) { - return ( -
- Failed to parse multipart response: {error} -
- ); - } - - const selectedPart = parts[activePart]; + if (error) { return (
-
-
- - -
- {selectedPart ? this._describePart(selectedPart) : 'Unknown'} -
- -
- {parts.map((part, i) => ( - - {i === activePart ? : } - {this._describePart(part)} - - ))} -
-
- - - - - - View Headers - - - Save as File - - -
- {selectedPart ? ( -
- -
- ) : null} + Failed to parse multipart response: {error}
); } + + if (parts.length === 0 || !selectedPart) { + return null; + } + return ( +
+
+
+ + +
+ {selectedPart.title} +
+ +
+ {parts.map(part => ( + + {selectedPart?.id === part.id ? : } + {part.title} + + ))} +
+
+ + + + + + View Headers + + + Save as File + + +
+
+ +
+ +
+ ); +}; + +function multipartBufferToArray({ bodyBuffer, contentType }: { bodyBuffer: Buffer | null; contentType: string }): Promise { + return new Promise((resolve, reject) => { + const parts: Part[] = []; + + if (!bodyBuffer) { + return resolve(parts); + } + + const fakeReq = new PassThrough(); + // @ts-expect-error -- TSCONVERSION investigate `stream` types + fakeReq.headers = { + 'content-type': contentType, + }; + const form = new multiparty.Form(); + let id = 0; + form.on('part', part => { + const dataBuffers: any[] = []; + part.on('data', data => { + dataBuffers.push(data); + }); + part.on('error', err => { + reject(new Error(`Failed to parse part: ${err.message}`)); + }); + part.on('end', () => { + const title = part.filename ? `${part.name} (${part.filename})` : part.name; + parts.push({ + id, + title, + value: dataBuffers ? Buffer.concat(dataBuffers) : Buffer.from(''), + name: part.name, + filename: part.filename || null, + bytes: part.byteCount, + headers: Object.keys(part.headers).map(name => ({ + name, + value: part.headers[name], + })), + }); + id += 1; + }); + }); + form.on('error', err => { + reject(err); + }); + form.on('close', () => { + resolve(parts); + }); + // @ts-expect-error -- TSCONVERSION + form.parse(fakeReq); + fakeReq.write(bodyBuffer); + fakeReq.end(); + }); }