From 187e482feb0905b0f4ffcf9b25dd64ad6d19a525 Mon Sep 17 00:00:00 2001 From: Its-treason <39559178+Its-treason@users.noreply.github.com> Date: Tue, 17 Oct 2023 00:07:51 +0200 Subject: [PATCH] feat(#596): Show real response in image preview --- .../QueryResult/QueryResultPreview/index.js | 66 +++++++ .../ResponsePane/QueryResult/index.js | 186 ++++++++---------- .../src/components/ResponsePane/index.js | 1 + .../RunnerResults/ResponsePane/index.js | 1 + packages/bruno-app/src/utils/network/index.js | 2 + .../bruno-electron/src/ipc/network/index.js | 9 + .../src/ipc/network/prepare-request.js | 3 +- 7 files changed, 158 insertions(+), 110 deletions(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js new file mode 100644 index 000000000..127d7e9c2 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js @@ -0,0 +1,66 @@ +import CodeEditor from 'components/CodeEditor/index'; +import { get } from 'lodash'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTheme } from 'providers/theme'; + +const QueryResultPreview = ({ + previewTab, + allowedPreviewModes, + data, + dataBuffer, + formattedData, + item, + contentType, + collection, + mode, + disableRunEventListener, + storedTheme +}) => { + const preferences = useSelector((state) => state.app.preferences); + const dispatch = useDispatch(); + + // Fail safe, so we don't render anything with an invalid tab + if (!allowedPreviewModes.includes(previewTab)) { + return null; + } + + const onRun = () => { + if (disableRunEventListener) { + return; + } + dispatch(sendRequest(item, collection.uid)); + }; + + switch (previewTab) { + case 'preview-web': { + const webViewSrc = data.replace('', ``); + return ( + + ); + } + case 'preview-image': { + return ; + } + default: + case 'raw': { + console.log(mode, storedTheme); + return ( + + ); + } + } +}; + +export default QueryResultPreview; diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index bb29abd3a..5e25f7f80 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -1,137 +1,105 @@ import React from 'react'; -import get from 'lodash/get'; -import CodeEditor from 'components/CodeEditor'; -import { useTheme } from 'providers/Theme'; -import { useDispatch, useSelector } from 'react-redux'; -import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import classnames from 'classnames'; import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common'; import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror'; +import QueryResultPreview from './QueryResultPreview'; import StyledWrapper from './StyledWrapper'; import { useState } from 'react'; import { useMemo } from 'react'; +import { useEffect } from 'react'; +import { useTheme } from 'providers/Theme/index'; + +const formatResponse = (data, mode) => { + if (!data) { + return ''; + } + + if (mode.includes('json')) { + return safeStringifyJSON(data, true); + } + + if (mode.includes('xml')) { + let parsed = safeParseXML(data, { collapseContent: true }); + if (typeof parsed === 'string') { + return parsed; + } + + return safeStringifyJSON(parsed, true); + } + + if (['text', 'html'].includes(mode) || typeof data === 'string') { + return data; + } + + return safeStringifyJSON(data); +}; const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => { - const { storedTheme } = useTheme(); - const preferences = useSelector((state) => state.app.preferences); - const [tab, setTab] = useState('preview'); - const dispatch = useDispatch(); const contentType = getContentType(headers); const mode = getCodeMirrorModeBasedOnContentType(contentType); + const formattedData = formatResponse(data, mode); + const { storedTheme } = useTheme(); - const formatResponse = (data, mode) => { - if (!data) { - return ''; + const allowedPreviewModes = useMemo(() => { + // Always show raw + const allowedPreviewModes = ['raw']; + + if (mode.includes('html') && typeof data === 'string') { + allowedPreviewModes.unshift('preview-web'); + } else if (mode.includes('image')) { + allowedPreviewModes.unshift('preview-image'); } - if (mode.includes('json')) { - return safeStringifyJSON(data, true); + return allowedPreviewModes; + }, [mode, data, formattedData]); + + const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]); + // Ensure the active Tab is always allowed + useEffect(() => { + if (!allowedPreviewModes.includes(previewTab)) { + setPreviewTab(allowedPreviewModes[0]); } + }, [previewTab, allowedPreviewModes]); - if (mode.includes('xml')) { - let parsed = safeParseXML(data, { collapseContent: true }); - - if (typeof parsed === 'string') { - return parsed; - } - - return safeStringifyJSON(parsed, true); - } - - if (['text', 'html'].includes(mode)) { - if (typeof data === 'string') { - return data; - } - - return safeStringifyJSON(data); - } - - if (mode.includes('image')) { - return item.requestSent.url; - } - - // final fallback - if (typeof data === 'string') { - return data; - } - - return safeStringifyJSON(data); - }; - - const value = formatResponse(data, mode); - - const onRun = () => { - if (disableRunEventListener) { - return; - } - dispatch(sendRequest(item, collection.uid)); - }; - - const getTabClassname = (tabName) => { - return classnames(`select-none ${tabName}`, { - active: tabName === tab, - 'cursor-pointer': tabName !== tab - }); - }; - - const getTabs = () => { - if (!mode.includes('html')) { + const tabs = useMemo(() => { + if (allowedPreviewModes.length === 1) { return null; } - return ( - <> -
setTab('raw')}> - Raw -
-
setTab('preview')}> - Preview -
- - ); - }; - - const activeResult = useMemo(() => { - if ( - tab === 'preview' && - mode.includes('html') && - item.requestSent && - item.requestSent.url && - typeof data === 'string' - ) { - // Add the Base tag to the head so content loads properly. This also needs the correct CSP settings - const webViewSrc = data.replace('', ``); - return ( - - ); - } else if (mode.includes('image')) { - return image; - } - - return ( - - ); - }, [tab, collection, storedTheme, onRun, value, mode]); + return allowedPreviewModes.map((previewMode) => ( +
setPreviewTab(previewMode)} + > + {previewMode.replace(/-(.*)/, ' ')} +
+ )); + }, [allowedPreviewModes, previewTab]); return (
- {getTabs()} + {tabs}
- {error ? {error} : activeResult} + {error ? ( + {error} + ) : ( + + )}
); }; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index e1cfab2ca..aea70de6f 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -43,6 +43,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { data={response.data} headers={response.headers} error={response.error} + key={item.filename} /> ); } diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index 2c4f28b20..6526c7454 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -35,6 +35,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { disableRunEventListener={true} data={responseReceived.data} headers={responseReceived.headers} + key={item.filename} /> ); } diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index c54c3338e..a0b0dfead 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -8,6 +8,8 @@ export const sendNetworkRequest = async (item, collection, environment, collecti resolve({ state: 'success', data: response.data, + // Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store + dataBuffer: response.dataBuffer, headers: Object.entries(response.headers), size: getResponseSize(response), status: response.status, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index adbf623e5..0addf4867 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -307,6 +307,14 @@ const registerNetworkIpc = (mainWindow) => { /** @type {import('axios').AxiosResponse} */ const response = await axiosInstance(request); + const dataBuffer = Buffer.from(response.data); + // Overwrite the original data for backwards compatability + response.data = dataBuffer.toString('utf-8'); + // Try to parse response to JSON, this can quitly fail + try { + response.data = JSON.parse(response.data); + } catch {} + // run post-response vars const postResponseVars = get(request, 'vars.res', []); if (postResponseVars?.length) { @@ -424,6 +432,7 @@ const registerNetworkIpc = (mainWindow) => { statusText: response.statusText, headers: response.headers, data: response.data, + dataBuffer: dataBuffer.toString('base64'), duration: requestDuration }; } catch (error) { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 3beab80f7..6c2d7d4b3 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -84,7 +84,8 @@ const prepareRequest = (request, collectionRoot) => { let axiosRequest = { method: request.method, url: request.url, - headers: headers + headers: headers, + responseType: 'arraybuffer' }; axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);