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 1/2] feat(#596): Show real response in image preview --- .../QueryResult/QueryResultPreview/index.js | 66 +++++++ .../ResponsePane/QueryResult/index.js | 180 +++++++----------- .../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, 155 insertions(+), 107 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 0000000000..127d7e9c28 --- /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 bb29abd3ab..5e25f7f80f 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'; - -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 formatResponse = (data, mode) => { - if (!data) { - return ''; +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; } - if (mode.includes('json')) { - return safeStringifyJSON(data, true); - } + return safeStringifyJSON(parsed, true); + } - if (mode.includes('xml')) { - let parsed = safeParseXML(data, { collapseContent: true }); + if (['text', 'html'].includes(mode) || typeof data === 'string') { + return data; + } - if (typeof parsed === 'string') { - return parsed; - } + return safeStringifyJSON(data); +}; - return safeStringifyJSON(parsed, true); - } +const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => { + const contentType = getContentType(headers); + const mode = getCodeMirrorModeBasedOnContentType(contentType); + const formattedData = formatResponse(data, mode); + const { storedTheme } = useTheme(); - if (['text', 'html'].includes(mode)) { - if (typeof data === 'string') { - return data; - } + const allowedPreviewModes = useMemo(() => { + // Always show raw + const allowedPreviewModes = ['raw']; - return safeStringifyJSON(data); + if (mode.includes('html') && typeof data === 'string') { + allowedPreviewModes.unshift('preview-web'); + } else if (mode.includes('image')) { + allowedPreviewModes.unshift('preview-image'); } - if (mode.includes('image')) { - return item.requestSent.url; - } + return allowedPreviewModes; + }, [mode, data, formattedData]); - // final fallback - if (typeof data === 'string') { - return data; + const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]); + // Ensure the active Tab is always allowed + useEffect(() => { + if (!allowedPreviewModes.includes(previewTab)) { + setPreviewTab(allowedPreviewModes[0]); } + }, [previewTab, allowedPreviewModes]); - 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 e1cfab2ca9..aea70de6f4 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 2c4f28b209..6526c74543 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 c54c3338ed..a0b0dfead7 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 adbf623e54..0addf48675 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 3beab80f7d..6c2d7d4b38 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); From b3ee0af22616bd26894129a6b021d89fa89e562f Mon Sep 17 00:00:00 2001 From: Its-treason <39559178+Its-treason@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:53:51 +0200 Subject: [PATCH 2/2] feat: Use real content size, fix runner, use the correct content charset --- .../QueryResult/QueryResultPreview/index.js | 3 +- .../ResponsePane/QueryResult/index.js | 1 + packages/bruno-app/src/utils/network/index.js | 6 +-- .../bruno-electron/src/ipc/network/index.js | 38 ++++++++++++++----- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js index 127d7e9c28..e17fc3454c 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js @@ -43,11 +43,10 @@ const QueryResultPreview = ({ ); } case 'preview-image': { - return ; + return ; } default: case 'raw': { - console.log(mode, storedTheme); return ( setPreviewTab(previewMode)} + key={previewMode} > {previewMode.replace(/-(.*)/, ' ')} diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index a0b0dfead7..ffd66743f9 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -11,7 +11,7 @@ export const sendNetworkRequest = async (item, collection, environment, collecti // 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), + size: response.size, status: response.status, statusText: response.statusText, duration: response.duration @@ -33,10 +33,6 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable }); }; -const getResponseSize = (response) => { - return response.headers['content-length'] || Buffer.byteLength(safeStringifyJSON(response.data)) || 0; -}; - export const fetchGqlSchema = async (endpoint, environment, request, collection) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 0addf48675..f40d3ed0c8 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -174,6 +174,20 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria return axiosInstance; }; +const parseDataFromResponse = (response) => { + const dataBuffer = Buffer.from(response.data); + // Parse the charset from content type: https://stackoverflow.com/a/33192813 + const charset = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || ''); + // Overwrite the original data for backwards compatability + let data = dataBuffer.toString(charset || 'utf-8'); + // Try to parse response to JSON, this can quitly fail + try { + data = JSON.parse(response.data); + } catch {} + + return { data, dataBuffer }; +}; + const registerNetworkIpc = (mainWindow) => { // handler for sending http request ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => { @@ -307,13 +321,8 @@ 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 {} + const { data, dataBuffer } = parseDataFromResponse(response); + response.data = data; // run post-response vars const postResponseVars = get(request, 'vars.res', []); @@ -433,6 +442,7 @@ const registerNetworkIpc = (mainWindow) => { headers: response.headers, data: response.data, dataBuffer: dataBuffer.toString('base64'), + size: Buffer.byteLength(dataBuffer), duration: requestDuration }; } catch (error) { @@ -448,6 +458,8 @@ const registerNetworkIpc = (mainWindow) => { } if (error?.response) { + const { data, dataBuffer } = parseDataFromResponse(error.response); + error.response.data = data; // run assertions const assertions = get(request, 'assertions'); if (assertions) { @@ -513,6 +525,8 @@ const registerNetworkIpc = (mainWindow) => { statusText: error.response.statusText, headers: error.response.headers, data: error.response.data, + dataBuffer: dataBuffer.toString('base64'), + size: Buffer.byteLength(dataBuffer), duration: requestDuration ?? 0 }; } @@ -729,6 +743,9 @@ const registerNetworkIpc = (mainWindow) => { const response = await axiosInstance(request); timeEnd = Date.now(); + const { data, dataBuffer } = parseDataFromResponse(response); + response.data = data; + // run post-response vars const postResponseVars = get(request, 'vars.res', []); if (postResponseVars?.length) { @@ -839,7 +856,7 @@ const registerNetworkIpc = (mainWindow) => { statusText: response.statusText, headers: Object.entries(response.headers), duration: timeEnd - timeStart, - size: response.headers['content-length'] || getSize(response.data), + size: Buffer.byteLength(dataBuffer), data: response.data } }); @@ -852,12 +869,15 @@ const registerNetworkIpc = (mainWindow) => { } if (error?.response) { + const { data, dataBuffer } = parseDataFromResponse(error.response); + error.response.data = data; + responseReceived = { status: error.response.status, statusText: error.response.statusText, headers: Object.entries(error.response.headers), duration: duration, - size: error.response.headers['content-length'] || getSize(error.response.data), + size: Buffer.byteLength(dataBuffer), data: error.response.data };