diff --git a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx index f390b42bc..2f75e5ad1 100644 --- a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx @@ -1,6 +1,12 @@ +import fs from 'fs'; import React, { FC, useCallback } from 'react'; +import { useSelector } from 'react-redux'; import { getPreviewModeName, PREVIEW_MODES, PreviewMode } from '../../../common/constants'; +import { exportHarCurrentRequest } from '../../../common/har'; +import * as models from '../../../models'; +import { isRequest } from '../../../models/request'; +import { selectActiveRequest, selectActiveResponse, selectResponsePreviewMode } from '../../redux/selectors'; import { Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; @@ -8,39 +14,85 @@ import { DropdownItem } from '../base/dropdown/dropdown-item'; interface Props { download: (pretty: boolean) => any; - fullDownload: (pretty: boolean) => any; - exportAsHAR: () => void; copyToClipboard: () => any; - updatePreviewMode: Function; - previewMode: PreviewMode; - showPrettifyOption?: boolean; } export const PreviewModeDropdown: FC = ({ - fullDownload, - previewMode, - showPrettifyOption, download, copyToClipboard, - exportAsHAR, - updatePreviewMode, }) => { + const request = useSelector(selectActiveRequest); + const previewMode = useSelector(selectResponsePreviewMode); + const response = useSelector(selectActiveResponse); - const handleClick = async (previewMode: string) => { - await updatePreviewMode(previewMode); + const handleClick = async (previewMode: PreviewMode) => { + if (!request || !isRequest(request)) { + return; + } + return models.requestMeta.updateOrCreateByParentId(request._id, { previewMode }); }; - const handleDownloadPrettify = useCallback(() => { - download(true); - }, [download]); + const handleDownloadPrettify = useCallback(() => download(true), [download]); - const handleDownloadNormal = useCallback(() => { - download(false); - }, [download]); + const handleDownloadNormal = useCallback(() => download(false), [download]); - const handleCopyRawResponse = useCallback(() => { - copyToClipboard(); - }, [copyToClipboard]); + const exportAsHAR = useCallback(async () => { + if (!response || !request || !isRequest(request)) { + console.warn('Nothing to download'); + return; + } + const data = await exportHarCurrentRequest(request, response); + const har = JSON.stringify(data, null, '\t'); + + const { filePath } = await window.dialog.showSaveDialog({ + title: 'Export As HAR', + buttonLabel: 'Save', + defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.har`, + }); + + if (!filePath) { + return; + } + const to = fs.createWriteStream(filePath); + to.on('error', err => { + console.warn('Failed to export har', err); + }); + to.end(har); + }, [request, response]); + + const exportDebugFile = useCallback(async () => { + if (!response || !request) { + console.warn('Nothing to download'); + return; + } + + const timeline = models.response.getTimeline(response); + const headers = timeline + .filter(v => v.name === 'HeaderIn') + .map(v => v.value) + .join(''); + + const { canceled, filePath } = await window.dialog.showSaveDialog({ + title: 'Save Full Response', + buttonLabel: 'Save', + defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.txt`, + }); + + if (canceled) { + return; + } + const readStream = models.response.getBodyStream(response); + + if (readStream && filePath) { + const to = fs.createWriteStream(filePath); + to.write(headers); + readStream.pipe(to); + to.on('error', err => { + console.warn('Failed to save full response', err); + }); + } + }, [request, response]); + const shouldPrettifyOption = response.contentType.includes('json'); return {getPreviewModeName(previewMode)} @@ -52,7 +104,7 @@ export const PreviewModeDropdown: FC = ({ {getPreviewModeName(mode, true)} )} Actions - + Copy raw response @@ -60,11 +112,11 @@ export const PreviewModeDropdown: FC = ({ Export raw response - {showPrettifyOption && + {shouldPrettifyOption && Export prettified response } - + Export HTTP debug diff --git a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx index af10b8616..11f7124b8 100644 --- a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx @@ -1,12 +1,13 @@ import { differenceInHours, differenceInMinutes, isThisWeek, isToday } from 'date-fns'; import React, { FC, Fragment, useCallback, useRef } from 'react'; +import { useSelector } from 'react-redux'; import { hotKeyRefs } from '../../../common/hotkeys'; import { executeHotKey } from '../../../common/hotkeys-listener'; import { decompressObject } from '../../../common/misc'; -import type { Environment } from '../../../models/environment'; -import type { RequestVersion } from '../../../models/request-version'; +import * as models from '../../../models/index'; import type { Response } from '../../../models/response'; +import { selectActiveEnvironment, selectActiveRequest, selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors'; import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; @@ -20,30 +21,23 @@ import { URLTag } from '../tags/url-tag'; import { TimeFromNow } from '../time-from-now'; interface Props { - activeEnvironment?: Environment | null; activeResponse: Response; className?: string; - handleDeleteResponse: Function; - handleDeleteResponses: Function; handleSetActiveResponse: Function; requestId: string; - requestVersions: RequestVersion[]; - responses: Response[]; } export const ResponseHistoryDropdown: FC = ({ - activeEnvironment, activeResponse, className, - handleDeleteResponse, - handleDeleteResponses, handleSetActiveResponse, requestId, - requestVersions, - responses, }) => { const dropdownRef = useRef(null); - + const activeEnvironment = useSelector(selectActiveEnvironment); + const responses = useSelector(selectActiveRequestResponses); + const activeRequest = useSelector(selectActiveRequest); + const requestVersions = useSelector(selectRequestVersions); const now = new Date(); const categories: Record = { minutes: [], @@ -53,6 +47,22 @@ export const ResponseHistoryDropdown: FC = ({ other: [], }; + const handleDeleteResponses = useCallback(async () => { + const environmentId = activeEnvironment ? activeEnvironment._id : null; + await models.response.removeForRequest(requestId, environmentId); + + if (activeRequest && activeRequest._id === requestId) { + await handleSetActiveResponse(requestId, null); + } + }, [activeEnvironment, activeRequest, handleSetActiveResponse, requestId]); + + const handleDeleteResponse = useCallback(async () => { + if (activeResponse) { + await models.response.remove(activeResponse); + } + handleSetActiveResponse(null); + }, [activeResponse, handleSetActiveResponse]); + responses.forEach(response => { const responseTime = new Date(response.created); @@ -148,7 +158,7 @@ export const ResponseHistoryDropdown: FC = ({ Delete Current Response - handleDeleteResponses(requestId, activeEnvironment ? activeEnvironment._id : null)}> + Clear History diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index d12f3cba8..e366aeca9 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -1,25 +1,18 @@ -import { autoBindMethodsForReact } from 'class-autobind-decorator'; import classnames from 'classnames'; import { clipboard } from 'electron'; import fs from 'fs'; import { json as jsonPrettify } from 'insomnia-prettify'; import { extension as mimeExtension } from 'mime-types'; -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import React, { FC, useCallback, useRef } from 'react'; +import { useSelector } from 'react-redux'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; -import { AUTOBIND_CFG, PREVIEW_MODE_SOURCE, PreviewMode } from '../../../common/constants'; -import { exportHarCurrentRequest } from '../../../common/har'; +import { PREVIEW_MODE_SOURCE } from '../../../common/constants'; import { getSetCookieHeaders } from '../../../common/misc'; import * as models from '../../../models'; -import type { Environment } from '../../../models/environment'; import type { Request } from '../../../models/request'; -import type { RequestVersion } from '../../../models/request-version'; -import type { Response } from '../../../models/response'; -import type { UnitTestResult } from '../../../models/unit-test-result'; import { cancelRequestById } from '../../../network/network'; -import { RootState } from '../../redux/modules'; -import { selectHotKeyRegistry } from '../../redux/selectors'; +import { selectActiveResponse, selectLoadStartTime, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors'; import { Button } from '../base/button'; import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown'; import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown'; @@ -32,63 +25,47 @@ import { TimeTag } from '../tags/time-tag'; import { ResponseCookiesViewer } from '../viewers/response-cookies-viewer'; import { ResponseHeadersViewer } from '../viewers/response-headers-viewer'; import { ResponseTimelineViewer } from '../viewers/response-timeline-viewer'; -import { ResponseViewer } from '../viewers/response-viewer'; +import { ResponseViewer } from '../viewers/response-viewer'; import { BlankPane } from './blank-pane'; import { Pane, paneBodyClasses, PaneHeader } from './pane'; import { PlaceholderResponsePane } from './placeholder-response-pane'; -interface OwnProps { - handleSetFilter: (filter: string) => void; - handleSetPreviewMode: Function; +interface Props { handleSetActiveResponse: Function; - handleDeleteResponses: Function; - handleDeleteResponse: Function; + handleSetFilter: (filter: string) => void; handleShowRequestSettings: Function; - previewMode: PreviewMode; - filter: string; - filterHistory: string[]; - disableHtmlPreviewJs: boolean; - editorFontSize: number; - loadStartTime: number; - responses: Response[]; - disableResponsePreviewLinks: boolean; - requestVersions: RequestVersion[]; request?: Request | null; - response?: Response | null; - environment?: Environment | null; - unitTestResult?: UnitTestResult | null; } +export const ResponsePane: FC = ({ + handleSetActiveResponse, + handleSetFilter, + handleShowRequestSettings, + request, +}) => { + const response = useSelector(selectActiveResponse); + const filterHistory = useSelector(selectResponseFilterHistory); + const filter = useSelector(selectResponseFilter); + const settings = useSelector(selectSettings); + const loadStartTime = useSelector(selectLoadStartTime); + const previewMode = useSelector(selectResponsePreviewMode); -const mapStateToProps = (state: RootState) => ({ - hotKeyRegistry: selectHotKeyRegistry(state), -}); - -type ReduxProps = ReturnType; - -type Props = OwnProps & ReduxProps; - -@autoBindMethodsForReact(AUTOBIND_CFG) -class UnconnectedResponsePane extends PureComponent { - _responseViewer: ResponseViewer | null = null; - - _setResponseViewerRef(responseViewer: ResponseViewer) { - this._responseViewer = responseViewer; - } - - _handleGetResponseBody(): Buffer | null { - if (!this.props.response) { + const responseViewerRef = useRef(null); + const handleGetResponseBody = useCallback(() => { + if (!response) { return null; } + return models.response.getBodyBuffer(response); + }, [response]); + const handleCopyResponseToClipboard = useCallback(async () => { + const bodyBuffer = handleGetResponseBody(); + if (bodyBuffer) { + clipboard.writeText(bodyBuffer.toString('utf8')); + } + }, [handleGetResponseBody]); - return models.response.getBodyBuffer(this.props.response); - } - - async _handleDownloadResponseBody(prettify: boolean) { - const { response, request } = this.props; - + const handleDownloadResponseBody = useCallback(async (prettify: boolean) => { if (!response || !request) { - // Should never happen - console.warn('No response to download'); + console.warn('Nothing to download'); return; } @@ -107,12 +84,11 @@ class UnconnectedResponsePane extends PureComponent { const readStream = models.response.getBodyStream(response); const dataBuffers: any[] = []; - if (readStream) { + if (readStream && outputPath) { readStream.on('data', data => { dataBuffers.push(data); }); readStream.on('end', () => { - // @ts-expect-error -- TSCONVERSION const to = fs.createWriteStream(outputPath); const finalBuffer = Buffer.concat(dataBuffers); to.on('error', err => { @@ -132,256 +108,136 @@ class UnconnectedResponsePane extends PureComponent { to.end(); }); } - } + }, [request, response]); - async _handleDownloadFullResponseBody() { - const { response, request } = this.props; - - if (!response || !request) { - // Should never happen - console.warn('No response to download'); - return; - } - - const timeline = models.response.getTimeline(response); - const headers = timeline - .filter(v => v.name === 'HeaderIn') - .map(v => v.value) - .join(''); - - const { canceled, filePath } = await window.dialog.showSaveDialog({ - title: 'Save Full Response', - buttonLabel: 'Save', - defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.txt`, - }); - - if (canceled) { - return; - } - - const readStream = models.response.getBodyStream(response); - - if (readStream) { - // @ts-expect-error -- TSCONVERSION - const to = fs.createWriteStream(filePath); - to.write(headers); - readStream.pipe(to); - to.on('error', err => { - console.warn('Failed to save full response', err); - }); - } - } - - async _handleCopyResponseToClipboard() { - if (!this.props.response) { - return; - } - - const bodyBuffer = models.response.getBodyBuffer(this.props.response); - if (bodyBuffer) { - clipboard.writeText(bodyBuffer.toString('utf8')); - } - } - - async _handleExportAsHAR() { - const { response, request } = this.props; - - if (!response) { - // Should never happen - console.warn('No response to download'); - return; - } - - if (!request) { - // Should never happen - console.warn('No request to download'); - return; - } - - const data = await exportHarCurrentRequest(request, response); - const har = JSON.stringify(data, null, '\t'); - - const { filePath } = await window.dialog.showSaveDialog({ - title: 'Export As HAR', - buttonLabel: 'Save', - defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.har`, - }); - - if (!filePath) { - return; - } - - const to = fs.createWriteStream(filePath); - to.on('error', err => { - console.warn('Failed to export har', err); - }); - to.end(har); - } - - _handleTabSelect(index: number, lastIndex: number) { - if (this._responseViewer != null && index === 0 && index !== lastIndex) { + const handleTabSelect = (index: number, lastIndex: number) => { + if (responseViewerRef.current != null && index === 0 && index !== lastIndex) { // Fix for CodeMirror editor not updating its content. // Refresh must be called when the editor is visible, // so use nextTick to give time for it to be visible. process.nextTick(() => { - // @ts-expect-error -- TSCONVERSION - this._responseViewer.refresh(); + responseViewerRef.current?.refresh(); }); } + }; + + if (!request) { + return ; } - render() { - const { - disableHtmlPreviewJs, - editorFontSize, - environment, - filter, - disableResponsePreviewLinks, - filterHistory, - handleDeleteResponse, - handleDeleteResponses, - handleSetActiveResponse, - handleSetFilter, - handleSetPreviewMode, - handleShowRequestSettings, - loadStartTime, - previewMode, - request, - requestVersions, - response, - responses, - } = this.props; - - if (!request) { - return ; - } - - if (!response) { - return ( - - cancelRequestById(request._id)} - loadStartTime={loadStartTime} - /> - - ); - } - - const cookieHeaders = getSetCookieHeaders(response.headers); + if (!response) { return ( - - {!response ? null : ( - -
- - - -
- -
- )} - - - - - - - - - - - - - - - - - - - -
- - - -
-
- -
- - - -
-
- - - - - -
- - cancelRequestById(request._id)} - loadStartTime={loadStartTime} - /> - -
+ + cancelRequestById(request._id)} + loadStartTime={loadStartTime} + /> + ); } -} -export const ResponsePane = connect(mapStateToProps)(UnconnectedResponsePane); + const cookieHeaders = getSetCookieHeaders(response.headers); + return ( + + {!response ? null : ( + +
+ + + +
+ +
+ )} + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+ + + +
+
+ + + + + +
+ + cancelRequestById(request._id)} + loadStartTime={loadStartTime} + /> + +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/wrapper-debug.tsx b/packages/insomnia/src/ui/components/wrapper-debug.tsx index e7295eb1b..2bf7fc093 100644 --- a/packages/insomnia/src/ui/components/wrapper-debug.tsx +++ b/packages/insomnia/src/ui/components/wrapper-debug.tsx @@ -7,7 +7,14 @@ import { isRemoteProject } from '../../models/project'; import { Request, RequestAuthentication, RequestBody, RequestHeader, RequestParameter } from '../../models/request'; import { Settings } from '../../models/settings'; import { isCollection, isDesign } from '../../models/workspace'; -import { selectActiveEnvironment, selectActiveRequest, selectActiveRequestResponses, selectActiveResponse, selectActiveUnitTestResult, selectActiveWorkspace, selectEnvironments, selectLoadStartTime, selectRequestVersions, selectResponseDownloadPath, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../redux/selectors'; +import { + selectActiveEnvironment, + selectActiveRequest, + selectActiveWorkspace, + selectEnvironments, + selectResponseDownloadPath, + selectSettings, +} from '../redux/selectors'; import { selectSidebarChildren, selectSidebarFilter } from '../redux/sidebar-selectors'; import { EnvironmentsDropdown } from './dropdowns/environments-dropdown'; import { SyncDropdown } from './dropdowns/sync-dropdown'; @@ -28,15 +35,12 @@ interface Props { gitSyncDropdown: ReactNode; handleActivityChange: HandleActivityChange; handleChangeEnvironment: Function; - handleDeleteResponse: Function; - handleDeleteResponses: Function; handleForceUpdateRequest: (r: Request, patch: Partial) => Promise; handleForceUpdateRequestHeaders: (r: Request, headers: RequestHeader[]) => Promise; handleImport: Function; handleSendAndDownloadRequestWithActiveEnvironment: (filepath?: string) => Promise; handleSendRequestWithActiveEnvironment: () => void; handleSetActiveResponse: Function; - handleSetPreviewMode: Function; handleSetResponseFilter: (filter: string) => void; handleShowRequestSettingsModal: Function; handleSidebarSort: (sortOrder: SortOrder) => void; @@ -55,15 +59,12 @@ export const WrapperDebug: FC = ({ gitSyncDropdown, handleActivityChange, handleChangeEnvironment, - handleDeleteResponse, - handleDeleteResponses, handleForceUpdateRequest, handleForceUpdateRequestHeaders, handleImport, handleSendAndDownloadRequestWithActiveEnvironment, handleSendRequestWithActiveEnvironment, handleSetActiveResponse, - handleSetPreviewMode, handleSetResponseFilter, handleShowRequestSettingsModal, handleSidebarSort, @@ -100,17 +101,11 @@ export const WrapperDebug: FC = ({ const activeEnvironment = useSelector(selectActiveEnvironment); const activeRequest = useSelector(selectActiveRequest); - const activeRequestResponses = useSelector(selectActiveRequestResponses); - const activeResponse = useSelector(selectActiveResponse); - const activeUnitTestResult = useSelector(selectActiveUnitTestResult); + const activeWorkspace = useSelector(selectActiveWorkspace); const environments = useSelector(selectEnvironments); - const loadStartTime = useSelector(selectLoadStartTime); - const requestVersions = useSelector(selectRequestVersions); + const responseDownloadPath = useSelector(selectResponseDownloadPath); - const responseFilter = useSelector(selectResponseFilter); - const responseFilterHistory = useSelector(selectResponseFilterHistory); - const responsePreviewMode = useSelector(selectResponsePreviewMode); const settings = useSelector(selectSettings); const sidebarChildren = useSelector(selectSidebarChildren); const sidebarFilter = useSelector(selectSidebarFilter); @@ -215,25 +210,10 @@ export const WrapperDebug: FC = ({ /> : } } /> diff --git a/packages/insomnia/src/ui/components/wrapper.tsx b/packages/insomnia/src/ui/components/wrapper.tsx index a81d2bfa8..8d1b77dea 100644 --- a/packages/insomnia/src/ui/components/wrapper.tsx +++ b/packages/insomnia/src/ui/components/wrapper.tsx @@ -27,7 +27,6 @@ import { RequestParameter, } from '../../models/request'; import { RequestGroup } from '../../models/request-group'; -import type { Response } from '../../models/response'; import { GitVCS } from '../../sync/git/git-vcs'; import { VCS } from '../../sync/vcs/vcs'; import { CookieModifyModal } from '../components/modals/cookie-modify-modal'; @@ -350,26 +349,6 @@ export class Wrapper extends PureComponent { showModal(RequestSettingsModal, { request: this.props.activeRequest }); } - async _handleDeleteResponses(requestId: string, environmentId: string | null) { - const { handleSetActiveResponse, activeRequest } = this.props; - await models.response.removeForRequest(requestId, environmentId); - - if (activeRequest && activeRequest._id === requestId) { - await handleSetActiveResponse(requestId, null); - } - } - - async _handleDeleteResponse(response: Response) { - if (response) { - await models.response.remove(response); - } - - // Also unset active response it's the one we're deleting - if (this.props.activeResponse?._id === response._id) { - this._handleSetActiveResponse(null); - } - } - async _handleRemoveActiveWorkspace() { const { activeWorkspace, handleSetActiveActivity } = this.props; @@ -654,8 +633,6 @@ export class Wrapper extends PureComponent { gitSyncDropdown={gitSyncDropdown} handleActivityChange={this._handleWorkspaceActivityChange} handleChangeEnvironment={this._handleChangeEnvironment} - handleDeleteResponse={this._handleDeleteResponse} - handleDeleteResponses={this._handleDeleteResponses} handleForceUpdateRequest={this._handleForceUpdateRequest} handleForceUpdateRequestHeaders={this._handleForceUpdateRequestHeaders} handleImport={this._handleImport}